@@ -904,6 +904,230 @@ pub fn build_streamlines(
904904 streamlines
905905}
906906
907+ fn build_dividing_streamline_internal < F > (
908+ field : & F ,
909+ nodes : & [ Point ] ,
910+ effective_body : Option < & [ Point ] > ,
911+ options : & StreamlineOptions ,
912+ ) -> Option < Vec < ( f64 , f64 ) > >
913+ where
914+ F : Fn ( f64 , f64 ) -> ( f64 , f64 ) ,
915+ {
916+ #[ derive( Debug , Clone , Copy , PartialEq , Eq ) ]
917+ enum StreamlineSide {
918+ Above ,
919+ Below ,
920+ }
921+
922+ #[ derive( Debug , Clone ) ]
923+ struct TraceResult {
924+ seed_y : f64 ,
925+ side : Option < StreamlineSide > ,
926+ closest_distance : f64 ,
927+ streamline : Vec < ( f64 , f64 ) > ,
928+ }
929+
930+ fn point_segment_distance_sq ( px : f64 , py : f64 , a : Point , b : Point ) -> f64 {
931+ let dx = b. x - a. x ;
932+ let dy = b. y - a. y ;
933+ let len_sq = dx * dx + dy * dy;
934+ if len_sq <= 1e-16 {
935+ let ex = px - a. x ;
936+ let ey = py - a. y ;
937+ return ex * ex + ey * ey;
938+ }
939+ let t = ( ( ( px - a. x ) * dx + ( py - a. y ) * dy) / len_sq) . clamp ( 0.0 , 1.0 ) ;
940+ let qx = a. x + t * dx;
941+ let qy = a. y + t * dy;
942+ let ex = px - qx;
943+ let ey = py - qy;
944+ ex * ex + ey * ey
945+ }
946+
947+ fn classify_streamline (
948+ streamline : & [ ( f64 , f64 ) ] ,
949+ nodes : & [ Point ] ,
950+ ) -> Option < ( Option < StreamlineSide > , f64 ) > {
951+ if streamline. len ( ) < 2 || nodes. len ( ) < 2 {
952+ return None ;
953+ }
954+
955+ let body_x_min = nodes. iter ( ) . map ( |p| p. x ) . fold ( f64:: INFINITY , f64:: min) ;
956+ let body_x_max = nodes. iter ( ) . map ( |p| p. x ) . fold ( f64:: NEG_INFINITY , f64:: max) ;
957+ let body_y_center = nodes. iter ( ) . map ( |p| p. y ) . sum :: < f64 > ( ) / nodes. len ( ) as f64 ;
958+ let chord = ( body_x_max - body_x_min) . abs ( ) . max ( 1e-6 ) ;
959+ let x_pad = 0.05 * chord;
960+ let x_ref = body_x_min + 0.65 * chord;
961+
962+ let mut best: Option < ( f64 , f64 ) > = None ;
963+ let consider_point = |best : & mut Option < ( f64 , f64 ) > , x : f64 , y : f64 | {
964+ let mut min_dist_sq = f64:: INFINITY ;
965+ for i in 0 ..nodes. len ( ) {
966+ let a = nodes[ i] ;
967+ let b = nodes[ ( i + 1 ) % nodes. len ( ) ] ;
968+ min_dist_sq = min_dist_sq. min ( point_segment_distance_sq ( x, y, a, b) ) ;
969+ }
970+ let signed_offset = y - body_y_center;
971+ if best. as_ref ( ) . is_none_or ( |( best_dist_sq, _) | min_dist_sq < * best_dist_sq) {
972+ * best = Some ( ( min_dist_sq, signed_offset) ) ;
973+ }
974+ } ;
975+
976+ for & ( x, y) in streamline {
977+ if x >= body_x_min - x_pad && x <= body_x_max + x_pad {
978+ consider_point ( & mut best, x, y) ;
979+ }
980+ }
981+ if best. is_none ( ) {
982+ for & ( x, y) in streamline {
983+ consider_point ( & mut best, x, y) ;
984+ }
985+ }
986+
987+ let ( dist_sq, signed_offset) = best?;
988+ let closest_distance = dist_sq. sqrt ( ) ;
989+ let end = * streamline. last ( ) ?;
990+ if end. 0 <= body_x_min + 0.12 * chord && closest_distance <= 0.04 * chord {
991+ let end_offset = end. 1 - body_y_center;
992+ if end_offset. abs ( ) <= 0.02 * chord {
993+ return Some ( ( None , closest_distance) ) ;
994+ }
995+ let side = if end_offset >= 0.0 {
996+ Some ( StreamlineSide :: Above )
997+ } else {
998+ Some ( StreamlineSide :: Below )
999+ } ;
1000+ return Some ( ( side, closest_distance) ) ;
1001+ }
1002+
1003+ let mut station_sample: Option < ( f64 , f64 ) > = None ;
1004+ for & ( x, y) in streamline {
1005+ let dx = ( x - x_ref) . abs ( ) ;
1006+ if station_sample
1007+ . as_ref ( )
1008+ . is_none_or ( |( best_dx, _) | dx < * best_dx)
1009+ {
1010+ station_sample = Some ( ( dx, y) ) ;
1011+ }
1012+ }
1013+ let station_y = station_sample. map ( |( _, y) | y) . unwrap_or ( body_y_center + signed_offset) ;
1014+ let side = if station_y >= body_y_center {
1015+ Some ( StreamlineSide :: Above )
1016+ } else {
1017+ Some ( StreamlineSide :: Below )
1018+ } ;
1019+ Some ( ( side, closest_distance) )
1020+ }
1021+
1022+ let bounds = ( options. x_min , options. x_max , options. y_min , options. y_max ) ;
1023+ let sample_count = options. seed_count . max ( 25 ) . min ( 129 ) ;
1024+ let y_span = ( options. y_max - options. y_min ) . abs ( ) ;
1025+ let seed_tol = ( 1e-4 * y_span) . max ( 1e-5 ) ;
1026+ let mut previous: Option < TraceResult > = None ;
1027+
1028+ let trace_seed = |seed_y : f64 | -> Option < TraceResult > {
1029+ if is_inside_airfoil ( options. seed_x , seed_y, nodes)
1030+ || effective_body. is_some_and ( |poly| is_inside_polygon ( options. seed_x , seed_y, poly) )
1031+ {
1032+ return None ;
1033+ }
1034+
1035+ let streamline = integrate_streamline (
1036+ field,
1037+ options. seed_x ,
1038+ seed_y,
1039+ options. step_size ,
1040+ options. max_steps ,
1041+ nodes,
1042+ bounds,
1043+ effective_body,
1044+ ) ;
1045+ if streamline. len ( ) < 2 {
1046+ return None ;
1047+ }
1048+
1049+ let ( side, closest_distance) = classify_streamline ( & streamline, nodes) ?;
1050+ Some ( TraceResult {
1051+ seed_y,
1052+ side,
1053+ closest_distance,
1054+ streamline,
1055+ } )
1056+ } ;
1057+
1058+ let mut bracket: Option < ( TraceResult , TraceResult ) > = None ;
1059+
1060+ for i in 0 ..sample_count {
1061+ let t = i as f64 / ( sample_count - 1 ) . max ( 1 ) as f64 ;
1062+ let seed_y = options. y_min + t * ( options. y_max - options. y_min ) ;
1063+ let Some ( trace) = trace_seed ( seed_y) else {
1064+ continue ;
1065+ } ;
1066+ if trace. side . is_none ( ) {
1067+ return Some ( trace. streamline ) ;
1068+ }
1069+
1070+ if let Some ( prev) = & previous {
1071+ if prev. side . is_some ( ) && trace. side . is_some ( ) && trace. side != prev. side {
1072+ bracket = Some ( ( prev. clone ( ) , trace) ) ;
1073+ break ;
1074+ }
1075+ }
1076+
1077+ previous = Some ( trace) ;
1078+ }
1079+
1080+ let Some ( ( mut lo, mut hi) ) = bracket else {
1081+ return None ;
1082+ } ;
1083+ if lo. seed_y > hi. seed_y {
1084+ std:: mem:: swap ( & mut lo, & mut hi) ;
1085+ }
1086+ let mut best = if lo. closest_distance <= hi. closest_distance {
1087+ lo. clone ( )
1088+ } else {
1089+ hi. clone ( )
1090+ } ;
1091+
1092+ for _ in 0 ..32 {
1093+ if ( hi. seed_y - lo. seed_y ) . abs ( ) <= seed_tol {
1094+ break ;
1095+ }
1096+
1097+ let y_mid = 0.5 * ( lo. seed_y + hi. seed_y ) ;
1098+ let Some ( mid) = trace_seed ( y_mid) else {
1099+ break ;
1100+ } ;
1101+ if mid. side . is_none ( ) {
1102+ return Some ( mid. streamline ) ;
1103+ }
1104+ if mid. closest_distance < best. closest_distance {
1105+ best = mid. clone ( ) ;
1106+ }
1107+
1108+ if mid. side == lo. side {
1109+ lo = mid;
1110+ } else {
1111+ hi = mid;
1112+ }
1113+ }
1114+
1115+ Some ( best. streamline )
1116+ }
1117+
1118+ /// Build the streamline whose stream-function value brackets `psi_0`.
1119+ pub fn build_dividing_streamline (
1120+ nodes : & [ Point ] ,
1121+ gamma : & [ f64 ] ,
1122+ alpha : f64 ,
1123+ v_inf : f64 ,
1124+ _psi_0 : f64 ,
1125+ options : & StreamlineOptions ,
1126+ ) -> Option < Vec < ( f64 , f64 ) > > {
1127+ let field = |x : f64 , y : f64 | velocity_at ( x, y, nodes, gamma, alpha, v_inf) ;
1128+ build_dividing_streamline_internal ( & field, nodes, None , options)
1129+ }
1130+
9071131/// Build streamlines using the viscous velocity field (vortex + source panels).
9081132pub fn build_streamlines_viscous (
9091133 nodes : & [ Point ] ,
@@ -966,6 +1190,25 @@ pub fn build_streamlines_viscous(
9661190 streamlines
9671191}
9681192
1193+ /// Build the viscous dividing streamline by bracketing `psi_0` on the inflow edge
1194+ /// and bisecting between streamlines that lie above and below the separatrix.
1195+ pub fn build_dividing_streamline_viscous (
1196+ nodes : & [ Point ] ,
1197+ gamma : & [ f64 ] ,
1198+ sigma : & [ f64 ] ,
1199+ alpha : f64 ,
1200+ v_inf : f64 ,
1201+ _psi_0 : f64 ,
1202+ wake_panels : Option < & WakePanels > ,
1203+ effective_body : Option < & [ Point ] > ,
1204+ options : & StreamlineOptions ,
1205+ ) -> Option < Vec < ( f64 , f64 ) > > {
1206+ let field = |x : f64 , y : f64 | {
1207+ velocity_at_with_sources ( x, y, nodes, gamma, sigma, alpha, v_inf, wake_panels)
1208+ } ;
1209+ build_dividing_streamline_internal ( & field, nodes, effective_body, options)
1210+ }
1211+
9691212#[ cfg( test) ]
9701213mod tests {
9711214 use super :: * ;
@@ -1056,4 +1299,67 @@ mod tests {
10561299 let finite_count = grid. iter ( ) . filter ( |& & v| v. is_finite ( ) ) . count ( ) ;
10571300 assert ! ( finite_count > 50 , "Should have many finite values outside airfoil" ) ;
10581301 }
1302+
1303+ #[ test]
1304+ fn test_build_dividing_streamline_brackets_circle_stagnation ( ) {
1305+ let circle = make_circle ( 96 , 0.5 ) ;
1306+ let gamma = vec ! [ 0.0 ; circle. len( ) ] ;
1307+ let options = StreamlineOptions {
1308+ seed_count : 25 ,
1309+ seed_x : -1.0 ,
1310+ y_min : -1.0 ,
1311+ y_max : 1.0 ,
1312+ step_size : 0.01 ,
1313+ max_steps : 400 ,
1314+ x_min : -1.2 ,
1315+ x_max : 1.5 ,
1316+ } ;
1317+
1318+ let streamline = build_dividing_streamline ( & circle, & gamma, 0.0 , 1.0 , 0.0 , & options)
1319+ . expect ( "expected dividing streamline" ) ;
1320+
1321+ let seed = streamline. first ( ) . copied ( ) . expect ( "seed point" ) ;
1322+ let last = streamline. last ( ) . copied ( ) . expect ( "terminal point" ) ;
1323+
1324+ assert ! ( seed. 1 . abs( ) < 1e-3 , "seed should converge to y=0, got {}" , seed. 1 ) ;
1325+ assert ! (
1326+ ( last. 0 + 0.5 ) . abs( ) < 0.05 ,
1327+ "streamline should end near stagnation x=-0.5, got {}" ,
1328+ last. 0
1329+ ) ;
1330+ assert ! ( last. 1 . abs( ) < 0.05 , "streamline should remain near y=0, got {}" , last. 1 ) ;
1331+ }
1332+
1333+ #[ test]
1334+ fn test_build_dividing_streamline_viscous_matches_zero_source_case ( ) {
1335+ let circle = make_circle ( 96 , 0.5 ) ;
1336+ let gamma = vec ! [ 0.0 ; circle. len( ) ] ;
1337+ let sigma = vec ! [ 0.0 ; circle. len( ) ] ;
1338+ let options = StreamlineOptions {
1339+ seed_count : 25 ,
1340+ seed_x : -1.0 ,
1341+ y_min : -1.0 ,
1342+ y_max : 1.0 ,
1343+ step_size : 0.01 ,
1344+ max_steps : 400 ,
1345+ x_min : -1.2 ,
1346+ x_max : 1.5 ,
1347+ } ;
1348+
1349+ let streamline = build_dividing_streamline_viscous (
1350+ & circle,
1351+ & gamma,
1352+ & sigma,
1353+ 0.0 ,
1354+ 1.0 ,
1355+ 0.0 ,
1356+ None ,
1357+ None ,
1358+ & options,
1359+ )
1360+ . expect ( "expected viscous dividing streamline" ) ;
1361+
1362+ let seed = streamline. first ( ) . copied ( ) . expect ( "seed point" ) ;
1363+ assert ! ( seed. 1 . abs( ) < 1e-3 , "seed should converge to y=0, got {}" , seed. 1 ) ;
1364+ }
10591365}
0 commit comments