88 ComponentSystem ,
99 GeneralRateModel ,
1010 LumpedRateModelWithPores ,
11+ LumpedRateModelWithoutPores ,
1112 Cstr
1213)
1314
15+ from CADETProcess .modelBuilder import LWE , BatchElution , CLR , FlipFlop , MRSSR
16+
1417FieldKind = str
1518Validator = Callable [[Any ], None ]
1619Transform = Callable [[Any ], Any ]
@@ -47,15 +50,22 @@ def parse_float_list(v: Any) -> list[float]:
4750 "c_feed" : FieldSpec ("c_feed" , "float_list" , "Feed concentration" , [1.0 ], transform = parse_float_list ),
4851 "c_load" : FieldSpec ("c_load" , "float_list" , "Load concentration" , [1.0 ], transform = parse_float_list ),
4952 "c_salt_low" : FieldSpec ("c_salt_low" , "float_list" , "Low-salt buffer" , [0.0 ], transform = parse_float_list ),
50- "c_salt_high" : FieldSpec ("c_salt_high" , "float_list" , "High-salt buffer" , [1 .0 ], transform = parse_float_list ),
51- "flow_rate" : FieldSpec ("flow_rate" , "float" , "Flow rate" , 1.0 , validate = require_positive ),
53+ "c_salt_high" : FieldSpec ("c_salt_high" , "float_list" , "High-salt buffer" , [0 .0 ], transform = parse_float_list ),
54+ "flow_rate" : FieldSpec ("flow_rate" , "float" , "Flow rate" , 1.0e-6 , validate = require_positive ),
5255 "feed_duration" : FieldSpec ("feed_duration" , "float" , "Feed duration" , 10.0 , validate = require_positive ),
5356 "load_duration" : FieldSpec ("load_duration" , "float" , "Load duration" , 10.0 , validate = require_positive ),
5457 "cycle_time" : FieldSpec ("cycle_time" , "float" , "Cycle time" , 100.0 , validate = require_positive ),
5558 "wash_duration" : FieldSpec ("wash_duration" , "float" , "Wash duration" , 10.0 , validate = require_positive ),
5659 "gradient_duration" : FieldSpec ("gradient_duration" , "float" , "Gradient duration" , 30.0 , validate = require_positive ),
5760 "final_wash_duration" : FieldSpec ("final_wash_duration" , "float" , "Final wash duration" , 0.0 ),
5861 "c_eluent" : FieldSpec ("c_eluent" , "float" , "Eluent (scalar)" , 0.0 ),
62+ "recycle_off" : FieldSpec ("recycle_off" , "float" , "Recycle off (t)" , 30.0 , validate = require_positive ),
63+ "pump_volume" : FieldSpec ("pump_volume" , "float" , "Pump volume" , 1e-9 , validate = require_positive ),
64+ "delay_flip" : FieldSpec ("delay_flip" , "float" , "Delay flip (t)" , 5.0 , validate = require_positive ),
65+ "delay_injection" : FieldSpec ("delay_injection" , "float" , "Delay injection (t)" , 5.0 , validate = require_positive ),
66+ "c_tank_init" : FieldSpec ("c_tank_init" , "float_list" , "Initial tank c" ,transform = parse_float_list )
67+
68+
5969}
6070
6171
@@ -72,8 +82,6 @@ def _pick(keys: Sequence[str]) -> list[FieldSpec]:
7282
7383def batch_elution_spec (column : ChromatographicColumnBase ) -> ModelSpec :
7484 def _build (v : Mapping [str , Any ]) -> Any :
75- # Import here to keep module import cheap
76- from CADETProcess .modelBuilder import BatchElution
7785 return BatchElution (
7886 column = column ,
7987 c_feed = v ["c_feed" ],
@@ -92,7 +100,6 @@ def _build(v: Mapping[str, Any]) -> Any:
92100
93101def lwe_spec (column : ChromatographicColumnBase ) -> ModelSpec :
94102 def _build (v : Mapping [str , Any ]) -> Any :
95- from CADETProcess .modelBuilder import LWE
96103 return LWE (
97104 column = column ,
98105 c_load = v ["c_load" ],
@@ -114,31 +121,122 @@ def _build(v: Mapping[str, Any]) -> Any:
114121 ]),
115122 build = _build ,
116123 )
124+
125+ def clr_spec (column : ChromatographicColumnBase ) -> ModelSpec :
126+ """Closed Loop Recycling (CLR) spec."""
127+ def _build (v : Mapping [str , Any ]) -> Any :
128+ from CADETProcess .modelBuilder import CLR
129+ return CLR (
130+ column = column ,
131+ c_feed = v ["c_feed" ],
132+ flow_rate = float (v ["flow_rate" ]),
133+ feed_duration = float (v ["feed_duration" ]),
134+ recycle_off = float (v ["recycle_off" ]),
135+ cycle_time = float (v ["cycle_time" ]),
136+ c_eluent = float (v ["c_eluent" ]),
137+ pump_volume = float (v ["pump_volume" ]),
138+ )
139+
140+ return ModelSpec (
141+ title = "Closed Loop Recycling (CLR)" ,
142+ fields = _pick ([
143+ "c_feed" ,
144+ "flow_rate" ,
145+ "feed_duration" ,
146+ "recycle_off" ,
147+ "cycle_time" ,
148+ "c_eluent" ,
149+ "pump_volume" ,
150+ ]),
151+ build = _build ,
152+ )
153+
154+ def flipflop_spec (column : ChromatographicColumnBase ) -> ModelSpec :
155+ """Flip-Flop spec."""
156+ def _build (v : Mapping [str , Any ]) -> Any :
157+ from CADETProcess .modelBuilder import FlipFlop
158+ return FlipFlop (
159+ column = column ,
160+ c_feed = v ["c_feed" ],
161+ flow_rate = float (v ["flow_rate" ]),
162+ feed_duration = float (v ["feed_duration" ]),
163+ delay_flip = float (v ["delay_flip" ]),
164+ delay_injection = float (v ["delay_injection" ]),
165+ c_eluent = float (v ["c_eluent" ]), # keep scalar eluent for now
166+ )
167+
168+ return ModelSpec (
169+ title = "Flip-Flop" ,
170+ fields = _pick ([
171+ "c_feed" ,
172+ "flow_rate" ,
173+ "feed_duration" ,
174+ "delay_flip" ,
175+ "delay_injection" ,
176+ "c_eluent" ,
177+ ]),
178+ build = _build ,
179+ )
180+
181+ def mrssr_spec (column : ChromatographicColumnBase ) -> ModelSpec :
182+ """Mixed-Recycle Steady-State Recycling (MRSSR) spec."""
183+ def _build (v : Mapping [str , Any ]) -> Any :
184+ from CADETProcess .modelBuilder import MRSSR
185+ # c_tank_init may be None (means: use feed concentration)
186+ return MRSSR (
187+ column = column ,
188+ c_feed = v ["c_feed" ],
189+ flow_rate = float (v ["flow_rate" ]),
190+ feed_duration = float (v ["feed_duration" ]),
191+ recycle_on = float (v ["recycle_on" ]),
192+ recycle_off = float (v ["recycle_off" ]),
193+ cycle_time = float (v ["cycle_time" ]),
194+ V_tank = float (v ["V_tank" ]),
195+ c_eluent = float (v ["c_eluent" ]),
196+ c_tank_init = v ["c_tank_init" ],
197+ )
198+
199+ return ModelSpec (
200+ title = "MRSSR" ,
201+ fields = _pick ([
202+ "c_feed" ,
203+ "flow_rate" ,
204+ "feed_duration" ,
205+ "recycle_on" ,
206+ "recycle_off" ,
207+ "cycle_time" ,
208+ "V_tank" ,
209+ "c_eluent" ,
210+ "c_tank_init" ,
211+ ]),
212+ build = _build ,
213+ )
214+
215+
117216
118217
119- # A simple registry the UI can use to populate the model picker.
120- # Keys are user-facing names; values are callables: (column) -> ModelSpec
121218MODEL_REGISTRY : dict [str , Callable [[ChromatographicColumnBase ], ModelSpec ]] = {
122219 "Batch Elution" : batch_elution_spec ,
123220 "Load–Wash–Elute (LWE)" : lwe_spec ,
124- # Add more later, e.g. "SMB": smb_spec, ...
221+ "CLR" : clr_spec ,
222+ "Flip-Flop" : flipflop_spec ,
223+ "MRSSR" : mrssr_spec ,
125224}
126225
127-
128- # ---- Column factories (receive a ComponentSystem and return a column) ----
129226ColumnFactory = Callable [[ComponentSystem ], ChromatographicColumnBase ]
130227
131228
132229def make_grm (cs : ComponentSystem ) -> ChromatographicColumnBase :
133230 col = GeneralRateModel (cs , name = "GRM" )
134- # Set minimal required parameters as needed, e.g.:
135- # col.length = 0.10
136231 return col
137232
138233
139234def make_lrmp (cs : ComponentSystem ) -> ChromatographicColumnBase :
140235 col = LumpedRateModelWithPores (cs , name = "LRMP" )
141- # col.length = 0.05
236+ return col
237+
238+ def make_lrm (cs : ComponentSystem ) -> ChromatographicColumnBase :
239+ col = LumpedRateModelWithoutPores (cs , name = "LRMP" )
142240 return col
143241
144242
@@ -151,59 +249,115 @@ def make_cstr(cs: ComponentSystem) -> ChromatographicColumnBase:
151249DEFAULT_COLUMN_FACTORIES : Dict [str , ColumnFactory ] = {
152250 "GRM" : make_grm ,
153251 "LRMP" : make_lrmp ,
252+ "LRM" : make_lrm ,
154253 "CSTR" : make_cstr ,
155- # "TubularReactor": make_tubular(...),
156- # "MCT": make_mct(...),
254+
157255}
158256
159- # --- Dynamic column configuration from required_parameters ---
257+ PARAM_TYPE_OVERRIDES : Dict [ str , FieldKind ] = {}
160258
161- # Optional overrides: parameter name -> FieldKind (e.g., "float", "float_list", "bool", "text")
162- # For now we default everything to "float", but you can override per name here.
163- PARAM_TYPE_OVERRIDES : Dict [str , FieldKind ] = {
164- # "length": "float",
165- # "diameter": "float",
166- # "porosity_bed": "float",
167- # "porosity_pore": "float",
168- # "volume": "float",
169- }
259+ def _unique_preserve_order ( names : Sequence [ str ]) -> list [ str ]:
260+ """Return names in original order, dropping duplicates (first occurrence wins)."""
261+ seen : set [str ] = set ()
262+ out : list [ str ] = []
263+ for n in names :
264+ if n not in seen :
265+ seen . add ( n )
266+ out . append ( n )
267+ return out
170268
171- def column_spec_from_required (column : ChromatographicColumnBase ) -> ModelSpec :
269+ def build_column_config_spec (column : ChromatographicColumnBase ) -> ModelSpec :
172270 """
173- Build a ModelSpec for a column by reflecting over column.required_parameters.
174- Defaults each field to kind='float' unless overridden in PARAM_TYPE_OVERRIDES.
175- Uses the current attribute value as the default if present.
271+ Build a ModelSpec for a column by reflecting over `column.required_parameters`.
272+
273+ - De-duplicates parameter names while preserving order.
274+ - Infers kind from the current attribute value:
275+ * list/tuple -> "float_list" (uses parse_float_list)
276+ * bool -> "bool"
277+ * otherwise -> "float" (default)
278+ (You can still override per name via PARAM_TYPE_OVERRIDES.)
279+ - Uses the column's current attribute value as the default (when present).
176280 """
281+
282+ def _unique_preserve_order (names : Sequence [str ]) -> list [str ]:
283+ seen : set [str ] = set ()
284+ out : list [str ] = []
285+ for n in names :
286+ if n not in seen :
287+ seen .add (n )
288+ out .append (n )
289+ return out
290+
177291 req = getattr (column , "required_parameters" , None )
178292 if not req :
179- # Fallback: nothing to configure
180293 return ModelSpec (
181294 title = f"Configure { column .__class__ .__name__ } " ,
182295 fields = [],
183296 build = lambda _v : column ,
184297 )
185298
299+ names = _unique_preserve_order (list (req ))
186300 fields : list [FieldSpec ] = []
187- for name in req :
188- kind = PARAM_TYPE_OVERRIDES . get ( name , "float" ) # default everything to float for now
301+
302+ for name in names :
189303 default = getattr (column , name , None )
304+
305+ # Default kind from overrides, else infer from current value
306+ kind = PARAM_TYPE_OVERRIDES .get (name )
307+ if kind is None :
308+ if isinstance (default , (list , tuple )):
309+ kind = "float_list"
310+ elif isinstance (default , bool ):
311+ kind = "bool"
312+ else :
313+ # treat numbers/strings/None as float for now
314+ kind = "float"
315+
190316 label = name .replace ("_" , " " ).capitalize ()
191- # You can extend with validation/transform per kind if you like.
192- fields .append (FieldSpec (name = name , kind = kind , label = label , default = default ))
317+
318+ # Attach transform for lists so the renderer feeds us a parsed list
319+ transform = parse_float_list if kind == "float_list" else None
320+
321+ fields .append (
322+ FieldSpec (
323+ name = name ,
324+ kind = kind ,
325+ label = label ,
326+ default = default ,
327+ transform = transform ,
328+ )
329+ )
193330
194331 def _apply (v : Mapping [str , Any ]) -> Any :
195- # Assign back to the column
196- for name in req :
197- if name in v :
198- try :
199- if PARAM_TYPE_OVERRIDES .get (name , "float" ) == "float" :
200- setattr (column , name , float (v [name ]))
332+ for name in names :
333+ if name not in v :
334+ continue
335+ try :
336+ kind = PARAM_TYPE_OVERRIDES .get (name )
337+ if kind is None :
338+ # infer again in case overrides are not set
339+ cur = getattr (column , name , None )
340+ if isinstance (cur , (list , tuple )):
341+ kind = "float_list"
342+ elif isinstance (cur , bool ):
343+ kind = "bool"
201344 else :
202- # room for future kinds
203- setattr (column , name , v [name ])
204- except Exception :
205- # keep going even if a parameter fails to set
206- pass
345+ kind = "float"
346+
347+ val = v [name ]
348+ if kind == "float_list" :
349+ # val may already be a list (thanks to transform), but normalize anyway
350+ val = parse_float_list (val )
351+ elif kind == "bool" :
352+ val = bool (val )
353+ else :
354+ # default "float"
355+ val = float (val )
356+
357+ setattr (column , name , val )
358+ except Exception :
359+ # keep going even if one assignment fails
360+ pass
207361 return column
208362
209363 return ModelSpec (
0 commit comments