@@ -1091,32 +1091,120 @@ def flat(self) -> pd.DataFrame:
10911091 df ["key" ] = df .labels .map (map_labels )
10921092 return df
10931093
1094+ def to_polars (self ) -> pl .DataFrame :
1095+ """
1096+ Convert all constraints to a single polars DataFrame.
1097+
1098+ The resulting dataframe is a long format with columns
1099+ `labels`, `coeffs`, `vars`, `rhs`, `sign`, `key`.
1100+ """
1101+ dfs = [self [k ].to_polars () for k in self ]
1102+ if not dfs :
1103+ return pl .DataFrame (
1104+ {
1105+ "labels" : pl .Series ([], dtype = pl .Int64 ),
1106+ "coeffs" : pl .Series ([], dtype = pl .Float64 ),
1107+ "vars" : pl .Series ([], dtype = pl .Int64 ),
1108+ "sign" : pl .Series ([], dtype = pl .String ),
1109+ "rhs" : pl .Series ([], dtype = pl .Float64 ),
1110+ "key" : pl .Series ([], dtype = pl .Int64 ),
1111+ }
1112+ )
1113+
1114+ df = pl .concat (dfs , how = "vertical_relaxed" )
1115+ labels = (
1116+ df .select ("labels" )
1117+ .unique (maintain_order = True )
1118+ .with_row_index (name = "key" , offset = 0 )
1119+ )
1120+ return df .join (labels , on = "labels" , how = "left" )
1121+
10941122 def to_matrix (self , filter_missings : bool = True ) -> scipy .sparse .csc_matrix :
10951123 """
10961124 Construct a constraint matrix in sparse format.
10971125
10981126 Missing values, i.e. -1 in labels and vars, are ignored filtered
10991127 out.
11001128 """
1101- # TODO: rename "filter_missings" to "~labels_as_coordinates"
1102- cons = self .flat
1103-
11041129 if not len (self ):
11051130 raise ValueError ("No constraints available to convert to matrix." )
11061131
1132+ def _build_dense_key_map (
1133+ arrays : list [np .ndarray ], total_size : int
1134+ ) -> tuple [np .ndarray , int ]:
1135+ mapping = np .full (total_size , - 1 , dtype = np .int64 )
1136+ next_key = 0
1137+ for labels in arrays :
1138+ labels = labels [labels != - 1 ]
1139+ n = labels .size
1140+ if n :
1141+ mapping [labels ] = np .arange (next_key , next_key + n )
1142+ next_key += n
1143+ return mapping , next_key
1144+
1145+ # Build sparse triplets directly from NumPy arrays to avoid dataframe overhead.
1146+ row_parts : list [np .ndarray ] = []
1147+ col_parts : list [np .ndarray ] = []
1148+ data_parts : list [np .ndarray ] = []
1149+ constraint_labels : list [np .ndarray ] = []
1150+
1151+ for _ , constraint in self .items ():
1152+ labels = constraint .labels .values .reshape (- 1 )
1153+ constraint_labels .append (labels )
1154+ vars_arr = constraint .vars .values
1155+ coeffs_arr = constraint .coeffs .values
1156+
1157+ term_axis = constraint .vars .get_axis_num (constraint .term_dim )
1158+ if term_axis != vars_arr .ndim - 1 :
1159+ vars_arr = np .moveaxis (vars_arr , term_axis , - 1 )
1160+ coeffs_arr = np .moveaxis (coeffs_arr , term_axis , - 1 )
1161+
1162+ nterm = vars_arr .shape [- 1 ]
1163+ row = np .repeat (labels , nterm )
1164+ col = vars_arr .reshape (- 1 )
1165+ data = coeffs_arr .reshape (- 1 )
1166+
1167+ mask = (row != - 1 ) & (col != - 1 ) & (data != 0 )
1168+ if mask .any ():
1169+ row_parts .append (row [mask ])
1170+ col_parts .append (col [mask ])
1171+ data_parts .append (data [mask ])
1172+
1173+ if row_parts :
1174+ row = np .concatenate (row_parts )
1175+ col = np .concatenate (col_parts )
1176+ data = np .concatenate (data_parts )
1177+ else :
1178+ row = np .array ([], dtype = np .int64 )
1179+ col = np .array ([], dtype = np .int64 )
1180+ data = np .array ([], dtype = float )
1181+
11071182 if filter_missings :
1108- vars = self .model .variables .flat
1109- shape = (cons .key .max () + 1 , vars .key .max () + 1 )
1110- cons ["vars" ] = cons .vars .map (vars .set_index ("labels" ).key )
1111- return scipy .sparse .csc_matrix (
1112- (cons .coeffs , (cons .key , cons .vars )), shape = shape
1183+ cons_map , next_con_key = _build_dense_key_map (
1184+ constraint_labels , self .model ._cCounter
11131185 )
1114- else :
1115- shape = self .model .shape
1116- return scipy .sparse .csc_matrix (
1117- (cons .coeffs , (cons .labels , cons .vars )), shape = shape
1186+ vars_map , next_var_key = _build_dense_key_map (
1187+ [
1188+ var .labels .values .reshape (- 1 )
1189+ for _ , var in self .model .variables .items ()
1190+ ],
1191+ self .model ._xCounter ,
11181192 )
11191193
1194+ shape = (next_con_key , next_var_key )
1195+
1196+ row = cons_map [row ]
1197+ col = vars_map [col ]
1198+ keep = (row != - 1 ) & (col != - 1 )
1199+ if not keep .all ():
1200+ row = row [keep ]
1201+ col = col [keep ]
1202+ data = data [keep ]
1203+ return scipy .sparse .csc_matrix ((data , (row , col )), shape = shape )
1204+
1205+ shape = self .model .shape
1206+ return scipy .sparse .csc_matrix ((data , (row , col )), shape = shape )
1207+
11201208 def reset_dual (self ) -> None :
11211209 """
11221210 Reset the stored solution of variables.
0 commit comments