@@ -249,10 +249,21 @@ def modifier_fn(c):
249249 st .header ("⚙️ Configuration" )
250250
251251 # Bundle selection
252- bundle_name = st .selectbox (
253- "Constraint Bundle " ,
252+ selected_bundles = st .multiselect (
253+ "Constraint Bundles " ,
254254 list (BUNDLES .keys ()),
255- help = "Select a predefined constraint set"
255+ default = list (BUNDLES .keys ()),
256+ help = "Select constraint bundles to include in multi-bundle analysis"
257+ )
258+
259+ if not selected_bundles :
260+ st .warning ("Please select at least one constraint bundle." )
261+ st .stop ()
262+
263+ bundle_name = st .selectbox (
264+ "Active Bundle for Single-Evaluation/Sweep" ,
265+ selected_bundles ,
266+ help = "Choose one of the selected bundles to focus on for Candidate Evaluation and Parameter Sweep"
256267 )
257268
258269 # Show bundle description
@@ -381,8 +392,11 @@ def modifier_fn(c):
381392st .caption ("Enter candidate properties below. Fields are derived from the selected constraint bundle." )
382393
383394# Dynamic Form Generation
384- constraints = bundle_info ["fn" ]()
385- property_names = sorted (list (set (c .name for c in constraints )))
395+ all_selected_constraints = []
396+ for b_name in selected_bundles :
397+ all_selected_constraints .extend (BUNDLES [b_name ]["fn" ]())
398+
399+ property_names = sorted (list (set (c .name for c in all_selected_constraints )))
386400
387401form_properties = {}
388402
@@ -413,10 +427,11 @@ def modifier_fn(c):
413427
414428st .markdown ("---" )
415429
416- tab_eval , tab_sweep , tab_custom = st .tabs ([
430+ tab_eval , tab_sweep , tab_custom , tab_matrix = st .tabs ([
417431 "🔍 Candidate Evaluation" ,
418432 "📈 Parameter Sweep & Boundary Mapping" ,
419- "👥 Custom Population Profiles"
433+ "👥 Custom Population Profiles" ,
434+ "📊 Multi-Bundle Matrix"
420435])
421436
422437with tab_eval :
@@ -1305,6 +1320,287 @@ def _render_tree_node(node: Dict[str, Any], level: int = 0):
13051320 st .markdown (f"**Test Result:** `{ test_result .status .value .upper ()} `" )
13061321 st .code (test_result .summary (), language = "text" )
13071322
1323+
1324+ # -----------------------------
1325+ # Tab: Multi-Bundle Matrix
1326+ # -----------------------------
1327+
1328+ with tab_matrix :
1329+ st .header ("📊 Multi-Bundle Evaluation Matrix" )
1330+ st .caption ("Evaluate a single candidate against all (or multiple selected) constraint bundles simultaneously." )
1331+
1332+ col_btn , col_info = st .columns ([1 , 3 ])
1333+ with col_btn :
1334+ generate_matrix_button = st .button (
1335+ "📊 Generate Multi-Bundle Matrix" ,
1336+ type = "primary" ,
1337+ use_container_width = True ,
1338+ key = "generate_matrix_btn"
1339+ )
1340+ with col_info :
1341+ st .caption (
1342+ "Runs evaluations across all selected bundles using the candidate properties defined above."
1343+ )
1344+
1345+ # We want to display the last generated results if they are in session state
1346+ if generate_matrix_button :
1347+ try :
1348+ # Parse candidate JSON
1349+ raw = json .loads (candidate_text )
1350+ cand = Candidate (
1351+ name = raw .get ("name" , "unnamed" ),
1352+ properties = raw .get ("properties" , {}),
1353+ provenance = raw .get ("provenance" )
1354+ )
1355+
1356+ matrix_results = {}
1357+ for b_name in selected_bundles :
1358+ constraints = BUNDLES [b_name ]["fn" ]()
1359+ cura = CuraFrame (constraints , name = f"CuraFrame::{ b_name } " )
1360+
1361+ # Register population modifiers
1362+ if use_population and population :
1363+ if population in POPULATION_MODIFIERS :
1364+ pop_mods = {
1365+ k : v for k , v in POPULATION_MODIFIERS [population ].items ()
1366+ if k != "description"
1367+ }
1368+ cura .add_population (population , pop_mods )
1369+ else :
1370+ custom_pops = db_auth .get_custom_populations (st .session_state ['user' ])
1371+ selected_custom_pop = next ((p for p in custom_pops if p ["name" ] == population ), None )
1372+ if selected_custom_pop :
1373+ pop_mods = {}
1374+ for mod in selected_custom_pop ["modifiers" ]:
1375+ param = mod ["parameter" ]
1376+ op = mod ["operator" ]
1377+ val = mod ["value" ]
1378+ pop_mods [param ] = make_custom_modifier (op , val )
1379+ if param == "clearance" :
1380+ pop_mods ["hepatic_clearance" ] = make_custom_modifier (op , val )
1381+ elif param == "hepatic_clearance" :
1382+ pop_mods ["clearance" ] = make_custom_modifier (op , val )
1383+ cura .add_population (population , pop_mods )
1384+
1385+ # Evaluate
1386+ pop_arg = population if use_population else None
1387+ result = cura .evaluate (cand , population = pop_arg , strict = strict )
1388+ matrix_results [b_name ] = {
1389+ "result" : result ,
1390+ "cura" : cura
1391+ }
1392+
1393+ # Save to session state
1394+ st .session_state ['last_matrix_results' ] = matrix_results
1395+ st .session_state ['last_matrix_candidate' ] = cand
1396+ st .session_state ['last_matrix_bundles' ] = selected_bundles
1397+ st .session_state ['last_matrix_pop' ] = population if use_population else None
1398+ st .session_state ['last_matrix_strict' ] = strict
1399+
1400+ except Exception as e :
1401+ st .error (f"❌ **Multi-bundle evaluation failed:** { e } " )
1402+ st .exception (e )
1403+
1404+ if 'last_matrix_results' in st .session_state :
1405+ matrix_results = st .session_state ['last_matrix_results' ]
1406+ cand = st .session_state ['last_matrix_candidate' ]
1407+ active_bundles = st .session_state ['last_matrix_bundles' ]
1408+ pop_arg = st .session_state ['last_matrix_pop' ]
1409+ strict_val = st .session_state ['last_matrix_strict' ]
1410+
1411+ import pandas as pd
1412+
1413+ # -----------------------------
1414+ # Layer 1: Summary Table
1415+ # -----------------------------
1416+ st .markdown ("---" )
1417+ st .subheader ("📋 Layer 1: Summary Table" )
1418+
1419+ summary_data = []
1420+ for b_name in active_bundles :
1421+ if b_name not in matrix_results :
1422+ continue
1423+ b_info = matrix_results [b_name ]
1424+ res = b_info ["result" ]
1425+
1426+ if res .status == EvaluationStatus .ACCEPTED :
1427+ emoji_status = "🟢 ACCEPTED"
1428+ elif res .status == EvaluationStatus .REJECTED :
1429+ emoji_status = "🔴 REJECTED"
1430+ else :
1431+ emoji_status = "🟡 INDET"
1432+
1433+ violated_params = ", " .join (sorted (list (set (v .constraint for v in res .violations )))) if res .violations else "None"
1434+ summary_data .append ({
1435+ "Bundle Name" : b_name ,
1436+ "Overall Status" : emoji_status ,
1437+ "Violations Count" : len (res .violations ),
1438+ "Violated Parameters" : violated_params
1439+ })
1440+
1441+ df_summary = pd .DataFrame (summary_data )
1442+ st .dataframe (df_summary , use_container_width = True , hide_index = True )
1443+
1444+ # -----------------------------
1445+ # Layer 2: Constraint Grid
1446+ # -----------------------------
1447+ st .markdown ("---" )
1448+ st .subheader ("🎯 Layer 2: Constraint Grid" )
1449+
1450+ # Get dynamic union of parameters
1451+ all_constraints = []
1452+ for b_name in active_bundles :
1453+ all_constraints .extend (BUNDLES [b_name ]["fn" ]())
1454+ unique_params = sorted (list (set (c .name for c in all_constraints )))
1455+
1456+ grid_data = []
1457+ for prop in unique_params :
1458+ row = {"Parameter" : prop }
1459+ for b_name in active_bundles :
1460+ if b_name not in matrix_results :
1461+ row [b_name ] = "⚪ —"
1462+ continue
1463+ b_info = matrix_results [b_name ]
1464+ cura = b_info ["cura" ]
1465+ res = b_info ["result" ]
1466+
1467+ c_obj = cura .get_constraint (prop )
1468+ if c_obj is None :
1469+ row [b_name ] = "⚪ —"
1470+ else :
1471+ if cand .get (prop ) is None :
1472+ row [b_name ] = "🟡 INDET"
1473+ else :
1474+ is_violated = any (v .constraint == prop for v in res .violations )
1475+ if is_violated :
1476+ row [b_name ] = "🔴 FAIL"
1477+ else :
1478+ row [b_name ] = "🟢 PASS"
1479+ grid_data .append (row )
1480+
1481+ df_grid = pd .DataFrame (grid_data )
1482+
1483+ def style_cells (val ):
1484+ if val == "🟢 PASS" :
1485+ return "background-color: #d4edda; color: #155724; font-weight: bold;"
1486+ elif val == "🔴 FAIL" :
1487+ return "background-color: #f8d7da; color: #721c24; font-weight: bold;"
1488+ elif val == "🟡 INDET" :
1489+ return "background-color: #fff3cd; color: #856404; font-weight: bold;"
1490+ elif val == "⚪ —" :
1491+ return "background-color: #e2e3e5; color: #383d41;"
1492+ return ""
1493+
1494+ if hasattr (df_grid .style , "map" ):
1495+ styled_grid = df_grid .style .map (style_cells , subset = active_bundles )
1496+ else :
1497+ styled_grid = df_grid .style .applymap (style_cells , subset = active_bundles )
1498+
1499+ st .dataframe (styled_grid , use_container_width = True , hide_index = True )
1500+
1501+ # -----------------------------
1502+ # Cross-Therapeutic Profile Warnings
1503+ # -----------------------------
1504+ st .markdown ("---" )
1505+ st .subheader ("⚠️ Cross-Therapeutic Profile Warnings" )
1506+
1507+ cross_warnings = []
1508+ accepted_list = []
1509+ rejected_list = []
1510+
1511+ for b_name in active_bundles :
1512+ if b_name not in matrix_results :
1513+ continue
1514+ res = matrix_results [b_name ]["result" ]
1515+ if res .status == EvaluationStatus .ACCEPTED :
1516+ accepted_list .append (b_name )
1517+ elif res .status == EvaluationStatus .REJECTED :
1518+ rejected_list .append (b_name )
1519+
1520+ for acc in accepted_list :
1521+ for rej in rejected_list :
1522+ rej_info = matrix_results [rej ]
1523+ crit_violations = [v for v in rej_info ["result" ].violations if v .severity == Severity .CRITICAL ]
1524+ for cv in crit_violations :
1525+ title = "Cross-Therapeutic Warning"
1526+ if "Cardiol" in rej :
1527+ title = "Cardiac Risk Warning"
1528+ elif "CNS" in rej :
1529+ title = "CNS Safety Warning"
1530+ elif "Safety" in rej :
1531+ title = "Core Safety Warning"
1532+
1533+ warning_text = (
1534+ f"**{ title } :** Candidate meets **{ acc } ** criteria but fails **{ rej } ** constraints "
1535+ f"due to a CRITICAL violation: **{ cv .constraint } ** (observed: { cv .observed } , required: { cv .threshold } ). "
1536+ f"\n \n *Rationale:* { cv .rationale } "
1537+ )
1538+ cross_warnings .append (warning_text )
1539+
1540+ if cross_warnings :
1541+ for cw in cross_warnings :
1542+ st .warning (cw )
1543+ else :
1544+ st .success ("No cross-therapeutic profile warnings detected. Candidate profile is consistent across all evaluated domains." )
1545+
1546+ # -----------------------------
1547+ # Export Capabilities
1548+ # -----------------------------
1549+ st .markdown ("---" )
1550+ st .subheader ("💾 Export Options" )
1551+
1552+ col_exp1 , col_exp2 , col_exp3 = st .columns (3 )
1553+
1554+ with col_exp1 :
1555+ # JSON Export
1556+ export_json_data = {
1557+ "candidate" : {
1558+ "name" : cand .name ,
1559+ "properties" : cand .properties ,
1560+ "provenance" : cand .provenance
1561+ },
1562+ "configuration" : {
1563+ "population" : pop_arg ,
1564+ "strict" : strict_val ,
1565+ "evaluated_bundles" : active_bundles
1566+ },
1567+ "matrix_summary" : summary_data ,
1568+ "grid" : grid_data ,
1569+ "cross_warnings" : cross_warnings
1570+ }
1571+ st .download_button (
1572+ "⬇️ Download Matrix Results (JSON)" ,
1573+ data = json .dumps (export_json_data , indent = 2 ),
1574+ file_name = f"curaframe_matrix_{ cand .name } .json" ,
1575+ mime = "application/json" ,
1576+ use_container_width = True ,
1577+ key = "download_json_matrix_btn"
1578+ )
1579+
1580+ with col_exp2 :
1581+ # Summary Table CSV Export
1582+ csv_summary = df_summary .to_csv (index = False )
1583+ st .download_button (
1584+ "⬇️ Download Summary Table (CSV)" ,
1585+ data = csv_summary ,
1586+ file_name = f"curaframe_matrix_summary_{ cand .name } .csv" ,
1587+ mime = "text/csv" ,
1588+ use_container_width = True ,
1589+ key = "download_csv_summary_btn"
1590+ )
1591+
1592+ with col_exp3 :
1593+ # Constraint Grid CSV Export
1594+ csv_grid = df_grid .to_csv (index = False )
1595+ st .download_button (
1596+ "⬇️ Download Constraint Grid (CSV)" ,
1597+ data = csv_grid ,
1598+ file_name = f"curaframe_matrix_grid_{ cand .name } .csv" ,
1599+ mime = "text/csv" ,
1600+ use_container_width = True ,
1601+ key = "download_csv_grid_btn"
1602+ )
1603+
13081604# Credits
13091605st .markdown ("---" )
13101606st .caption (
0 commit comments