refactor: 데모 브랜치 패키지 통일 com.example.medilightv2android → com.medithings.vesiscan
사용자앱 (feature/tab-navigation) 과 패키지 이름 일치. namespace + applicationId 둘 다 com.medithings.vesiscan 로 변경 (기존 데모 앱은 재설치 필요). ## 변경 범위 - Kotlin 148 파일: package + import 문 (714 occurrences) - 디렉토리 이동: com/example/medilightv2android → com/medithings/vesiscan (main, test, androidTest 각각) - app/build.gradle.kts: namespace, applicationId - docs/FLAVOR_DEMO_STABLE.md: 참조 갱신 - V41DetectorCH4Test: BvDispatchResult.methodChosen → method (dto field name fix) ## 주의 - applicationId 가 바뀌므로 기존 데모 앱 (com.example.medilightv2android.demo) 은 Android 관점에서 다른 앱으로 취급 — 재설치 시 PIN/설정 초기화됨. - Fresh install 권장. 기존 앱 (com.example...) 은 별도로 uninstall 필요. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,434 @@
|
||||
/*
|
||||
* Method D multichannel runner + cross-channel corrections.
|
||||
*
|
||||
* 1:1 port of piezophantomtest `library/runners.py::method_d()` +
|
||||
* `library/cross_channel.py` (2026-07-02 4830e7c + 5576a59)
|
||||
*
|
||||
* 파이프라인:
|
||||
* 1) heavy = SG(7,3) + oscfar_median(win=9, iter=4) ← 02bed02 win 5→9
|
||||
* light = SG(7,3)
|
||||
* 2) apply_tgc_pipeline(heavy / light) — center_ch=None → all channels
|
||||
* 3) 채널별 otsu_ratio × cos(beam_angle) ← PiezoHW.degreeAll
|
||||
* 4) MethodDDetector.detect()
|
||||
* 5) cross-channel 후처리 (순서 고정):
|
||||
* ① applyAntTiebreak — 전벽 교차보정 (4830e7c 신 버전)
|
||||
* ② applyNeighborTopValidate — 최상단 채널 FP/FN 검증 (신규 4830e7c)
|
||||
* ③ applyInwardPost — 최상단 widest post 과확장 교정 (신규 5576a59)
|
||||
*
|
||||
* 출력 컨트랙트: alignment.py 가 `dets[i].urine_len` 만 의존 →
|
||||
* MethodDResult.urineLen 또는 null 의 List 로 충분.
|
||||
*/
|
||||
package com.medithings.vesiscan.walldetect
|
||||
|
||||
import com.medithings.vesiscan.managers.AlignmentConstants
|
||||
import com.medithings.vesiscan.managers.PiezoHW
|
||||
import com.medithings.vesiscan.walldetect.algo.methodd.MethodDParams
|
||||
import com.medithings.vesiscan.walldetect.algo.methodd.MethodDPreprocessing
|
||||
import com.medithings.vesiscan.walldetect.algo.methodd.MethodDTgc
|
||||
import com.medithings.vesiscan.walldetect.algo.methodd.MethodDWallSelect
|
||||
import com.medithings.vesiscan.walldetect.algo.methodd.MethodDWallSelect.CType
|
||||
import com.medithings.vesiscan.walldetect.algo.methodd.MethodDWallSelect.Side
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.min as kmin
|
||||
|
||||
object MethodDRunner {
|
||||
|
||||
// ── cross-channel tuning constants (cross_channel.py:30-45) ────────────────
|
||||
/** 깊은쪽 교정 시 "합의에 이 이상 더 가까운 후보" 요구 (곡률 보존). */
|
||||
private const val ANT_OUTLIER_IMPROVE_MARGIN_MM = 5.0
|
||||
/** 교정 대상이 합의에서 이 이상 떨어지면 보정 보류 (얕은쪽은 제외). */
|
||||
private const val ANT_CONSENSUS_CLOSE_TOL_MM = 6.0
|
||||
/** dominant-pick 보호: raw 픽 score >= 이 배수 × 차순위 → 합의 교정에서 보존. */
|
||||
private const val ANT_DOMINANT_RATIO = 2.0
|
||||
|
||||
/** neighbor_top_validate: nb-1→nb slope 가 이 이하면 "비증가 추세". */
|
||||
private const val NTV_SLOPE_EPS = 2.0
|
||||
/** neighbor_top_validate: top 이 예측보다 이만큼 깊으면 위반 (FP). */
|
||||
private const val NTV_DEV_DELTA = 6.0
|
||||
|
||||
/** cross-channel 3개 스위치 (Python 과 동일 default). */
|
||||
private const val NEIGHBOR_TOP_VALIDATE = true
|
||||
private const val INWARD_POST_SEARCH = true
|
||||
|
||||
/**
|
||||
* 6채널 (또는 N채널) raw 신호 → 채널별 MethodDResult? 리스트.
|
||||
* raw[ch] 길이가 다르면 그대로 처리 (각 채널 독립).
|
||||
*/
|
||||
fun detectMultichannel(
|
||||
signals: List<DoubleArray>,
|
||||
params: MethodDParams = MethodDParams.DEFAULT,
|
||||
beamAnglesDeg: DoubleArray? = null,
|
||||
applyTgc: Boolean = true,
|
||||
): List<MethodDResult?> {
|
||||
val angles = beamAnglesDeg ?: PiezoHW.degreeAll
|
||||
// 1) per-channel SG denoise (heavy + light)
|
||||
val heavyList = signals.map { MethodDPreprocessing.preprocessHeavy(it, params) }
|
||||
val lightList = signals.map { MethodDPreprocessing.preprocessLight(it) }
|
||||
// 2) TGC per channel (Python apply_tgc_pipeline default center_ch=None → all)
|
||||
val heavyTgc = if (applyTgc) MethodDTgc.applyTgcPipeline(heavyList) else heavyList
|
||||
val lightTgc = if (applyTgc) MethodDTgc.applyTgcPipeline(lightList) else lightList
|
||||
// 3+4) per-channel cos-angle adjusted otsu_ratio + detect
|
||||
val results = MutableList(signals.size) { ch ->
|
||||
val angleDeg = if (ch < angles.size) angles[ch] else 0.0
|
||||
val chRatio = params.otsuRatio * cos(Math.toRadians(angleDeg))
|
||||
MethodDDetector.detect(
|
||||
raw = signals[ch],
|
||||
denoisedHeavy = heavyTgc[ch],
|
||||
denoisedLight = lightTgc[ch],
|
||||
otsuRatioOverride = chRatio,
|
||||
params = params,
|
||||
)
|
||||
}
|
||||
// 5) cross-channel 후처리 (Python `apply_cross_channel` 순서 고정)
|
||||
applyCrossChannel(results, angles, lightTgc, heavyTgc, signals, params)
|
||||
return results
|
||||
}
|
||||
|
||||
/**
|
||||
* Cross-channel 3-stage 후처리 (Python `apply_cross_channel`).
|
||||
* ① ant_tiebreak → ② neighbor_top_validate → ③ inward_post
|
||||
*/
|
||||
private fun applyCrossChannel(
|
||||
results: MutableList<MethodDResult?>,
|
||||
angles: DoubleArray,
|
||||
light: List<DoubleArray>,
|
||||
heavy: List<DoubleArray>,
|
||||
raw6ch: List<DoubleArray>,
|
||||
params: MethodDParams,
|
||||
) {
|
||||
applyAntTiebreak(results, angles)
|
||||
if (NEIGHBOR_TOP_VALIDATE) {
|
||||
applyNeighborTopValidate(results, angles, light, heavy, raw6ch, params)
|
||||
}
|
||||
if (INWARD_POST_SEARCH) {
|
||||
applyInwardPost(results, angles)
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// ① ant_tiebreak — 전벽 교차보정 (2026-07-02: 4830e7c 신 버전)
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
/**
|
||||
* 이웃 center 채널 전벽 z 합의 (LOO median) 기준 전벽 보정 (in-place).
|
||||
*
|
||||
* 단일 임계 `devTol=8mm` + 방향 분기 (구 nf_tol 8 / outlier_tol 9 통합):
|
||||
* [최우선] dominant-pick 보호: raw 픽 score >= 2.0 × 차순위 → 보존
|
||||
* (1) 동률(top1/top2 < tieRatio): 동률 band 중 합의 최근접 선택
|
||||
* (2) 얕은쪽 (합의 - dev_tol 미만 = near-field 아티팩트): 무조건 재선택
|
||||
* (3) 깊은쪽 (합의 + dev_tol 초과): improve_margin 이상 더 가까운 후보 있을 때만
|
||||
*
|
||||
* 얕은쪽 제외 후 합의 최근접 pick. 합의-근접 가드 (얕은쪽 교정은 항상, 그 외엔 pick 이
|
||||
* 합의 근접일 때만).
|
||||
*/
|
||||
private fun applyAntTiebreak(
|
||||
results: MutableList<MethodDResult?>,
|
||||
angles: DoubleArray,
|
||||
tieRatio: Double = AlignmentConstants.ANT_TIEBREAK_RATIO,
|
||||
devTol: Double = AlignmentConstants.ANT_NEARFIELD_TOL_MM,
|
||||
improveMargin: Double = ANT_OUTLIER_IMPROVE_MARGIN_MM,
|
||||
) {
|
||||
if (results.isEmpty()) return
|
||||
|
||||
// 현재 center 채널 ant z 좌표 (LOO median 의 base)
|
||||
val baseZ = HashMap<Int, Double>()
|
||||
for (i in AlignmentConstants.CENTER_CH) {
|
||||
if (i < results.size) {
|
||||
val r = results[i] ?: continue
|
||||
baseZ[i] = zOf(angles, i, r.ant)
|
||||
}
|
||||
}
|
||||
|
||||
for (i in AlignmentConstants.CENTER_CH) {
|
||||
if (i >= results.size) continue
|
||||
val r = results[i] ?: continue
|
||||
val cands = r.antCandidates
|
||||
if (cands.size < 2) continue
|
||||
|
||||
val cons = AlignmentConstants.CENTER_CH
|
||||
.filter { it != i }
|
||||
.mapNotNull { baseZ[it] }
|
||||
if (cons.isEmpty()) continue
|
||||
val cz = median(cons)
|
||||
|
||||
// dominant-pick 보호 (최우선)
|
||||
if (cands[0].second >= ANT_DOMINANT_RATIO * cands[1].second
|
||||
&& r.ant == cands[0].first) {
|
||||
continue
|
||||
}
|
||||
|
||||
val cur = zOf(angles, i, r.ant)
|
||||
val curDev = abs(cur - cz)
|
||||
val bestDev = cands.minOf { abs(zOf(angles, i, it.first) - cz) }
|
||||
val isTie = cands[0].second / maxOf(cands[1].second, 1e-9) < tieRatio
|
||||
val isShallow = cur < cz - devTol
|
||||
val isDeep = cur > cz + devTol
|
||||
|
||||
// 진입 + 후보 pool (방향 분기)
|
||||
val pool: List<Pair<Int, Double>> = when {
|
||||
isTie -> {
|
||||
val thr = cands[0].second / tieRatio
|
||||
cands.filter { it.second >= thr }
|
||||
}
|
||||
isShallow -> cands.toList() // 얕은 아티팩트 → 무조건
|
||||
isDeep && bestDev < curDev - improveMargin -> cands.toList()
|
||||
else -> continue // 압승 & 정상 범위 → 유지
|
||||
}
|
||||
|
||||
// 얕은(아티팩트) 후보 제외, 합의 최근접 선택
|
||||
val eligPrelim = pool.filter { zOf(angles, i, it.first) >= cz - devTol }
|
||||
val elig = if (eligPrelim.isEmpty()) pool else eligPrelim
|
||||
val pick = elig.minBy { abs(zOf(angles, i, it.first) - cz) }
|
||||
|
||||
// 합의-근접 가드: 얕은쪽 교정은 항상, 그 외엔 pick 이 합의 근접일 때만
|
||||
val pickDev = abs(zOf(angles, i, pick.first) - cz)
|
||||
if (pickDev > ANT_CONSENSUS_CLOSE_TOL_MM && !isShallow) continue
|
||||
|
||||
if (pick.first != r.ant && r.post > pick.first) {
|
||||
r.ant = pick.first
|
||||
r.antRefined = pick.first.toDouble()
|
||||
r.urineLen = r.post - pick.first - 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// ② neighbor_top_validate — 최상단 채널 FP/FN 검증 (신규 4830e7c)
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
/**
|
||||
* 이웃 후위벽 trend 로 최상단 채널 검증 (in-place).
|
||||
*
|
||||
* Rule B (trend-FP): 검출 top 후위벽 z 를 아래 두 채널 기울기로 예측. 비증가추세인데
|
||||
* top 이 예측보다 깊게(>6mm) jump → nb urine span 시드 재탐색 → drop/교체.
|
||||
* Rule A (FN): 미검출 top 을 바로 아래 검출 채널 span 시드로 재탐색 복원(gate 통과 시).
|
||||
*/
|
||||
private fun applyNeighborTopValidate(
|
||||
results: MutableList<MethodDResult?>,
|
||||
angles: DoubleArray,
|
||||
light: List<DoubleArray>,
|
||||
heavy: List<DoubleArray>,
|
||||
raw6ch: List<DoubleArray>,
|
||||
params: MethodDParams,
|
||||
) {
|
||||
val n = kmin(4, results.size)
|
||||
val origNone = (0 until n).filter { results[it] == null }.toHashSet()
|
||||
val det = (0 until n).filter { results[it] != null }.sorted()
|
||||
|
||||
// ---- Rule B ----
|
||||
if (det.size >= 3) {
|
||||
val top = det[0]; val nb1 = det[1]; val nb2 = det[2]
|
||||
val rTop = results[top]!!
|
||||
val rNb1 = results[nb1]!!
|
||||
val rNb2 = results[nb2]!!
|
||||
val zTop = zOf(angles, top, rTop.post)
|
||||
val zNb1 = zOf(angles, nb1, rNb1.post)
|
||||
val zNb2 = zOf(angles, nb2, rNb2.post)
|
||||
val slope = zNb1 - zNb2
|
||||
val pred = zNb1 + slope
|
||||
if (slope <= NTV_SLOPE_EPS && (zTop - pred) > NTV_DEV_DELTA) {
|
||||
val rr = researchPostInSpan(
|
||||
light[top], heavy[top], raw6ch[top], rNb1.lowStart, rNb1.lowEnd, params
|
||||
)
|
||||
if (rr == null || (zOf(angles, top, rr.post) - pred) > NTV_DEV_DELTA) {
|
||||
results[top] = null
|
||||
} else {
|
||||
rTop.post = rr.post
|
||||
rTop.postRefined = rr.post.toDouble()
|
||||
rTop.postType = rr.postType
|
||||
rTop.urineLen = rTop.post - rTop.ant - 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Rule A ----
|
||||
val first = (0 until n).firstOrNull { results[it] != null }
|
||||
if (first != null && first >= 1 && (first - 1) in origNone) {
|
||||
val top = first - 1
|
||||
val nb = first
|
||||
val rNb = results[nb]!!
|
||||
val ls = rNb.lowStart
|
||||
val le = rNb.lowEnd
|
||||
val rr = researchPostInSpan(light[top], heavy[top], raw6ch[top], ls, le, params)
|
||||
if (rr != null) {
|
||||
val lowAmp = minInRange(heavy[top], ls, le)
|
||||
results[top] = MethodDResult(
|
||||
ant = rr.ant,
|
||||
post = rr.post,
|
||||
antRefined = rr.ant.toDouble(),
|
||||
postRefined = rr.post.toDouble(),
|
||||
lowStart = ls,
|
||||
lowEnd = le,
|
||||
lowAmp = lowAmp,
|
||||
inwardWalkAnt = 0,
|
||||
inwardWalkPost = 0,
|
||||
urineLen = rr.post - rr.ant - 1,
|
||||
antProm = 0.0,
|
||||
postProm = 0.0,
|
||||
antType = rr.antType,
|
||||
postType = rr.postType,
|
||||
sgHeavy = heavy[top],
|
||||
sgLight = light[top],
|
||||
antCandidates = emptyList(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** _research_post_in_span 결과. */
|
||||
private data class ResearchResult(val ant: Int, val post: Int, val antType: CType, val postType: CType)
|
||||
|
||||
/**
|
||||
* seed span(ls, le) 에서 ant / post 재탐색 + wall/raw gate. 실패 시 null.
|
||||
* detector 와 동일한 gate (_wall_gate_ok + min_post_raw_ratio) 로 재검증.
|
||||
*/
|
||||
private fun researchPostInSpan(
|
||||
lt: DoubleArray, hv: DoubleArray, raw: DoubleArray,
|
||||
ls: Int, le: Int, params: MethodDParams,
|
||||
): ResearchResult? {
|
||||
if (le <= ls) return null
|
||||
val antRes = MethodDWallSelect.findWallPeakLocal(
|
||||
lt, ls, le, Side.ANT, params,
|
||||
dMaxOverride = params.antDMax, inwardWalk = 0,
|
||||
)
|
||||
val postRes = MethodDWallSelect.findWallPeakLocal(
|
||||
lt, ls, le, Side.POST, params,
|
||||
dMaxOverride = params.dMax, inwardWalk = 0,
|
||||
)
|
||||
val antBest = antRes.best ?: return null
|
||||
val postBest = postRes.best ?: return null
|
||||
val ai = antBest.idx
|
||||
val pi = postBest.idx
|
||||
if (pi <= ai || (pi - ai - 1) < params.minUrineLen) return null
|
||||
|
||||
// wall gate
|
||||
val lumenMin = minInRange(hv, ls, le)
|
||||
val wallAmp = if (params.wallRatioUsePostOnly) lt[pi]
|
||||
else kmin(lt[ai], lt[pi])
|
||||
if (wallAmp / maxOf(lumenMin, 1.0) < params.minWallLumenRatio) return null
|
||||
|
||||
// raw ratio gate (SG-only 재계산)
|
||||
val rawHv = MethodDPreprocessing.preprocessHeavy(raw, params)
|
||||
val rawLt = MethodDPreprocessing.preprocessLight(raw)
|
||||
val rawLumen = minInRange(rawHv, ls, le)
|
||||
if (rawLt[pi] / maxOf(rawLumen, 1.0) < params.minPostRawRatio) return null
|
||||
|
||||
return ResearchResult(ai, pi, antBest.type, postBest.type)
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// ③ inward_post — 최상단 widest 후벽 과확장 교정 (신규 5576a59)
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
/**
|
||||
* 최상단 검출 center 채널이 widest (적도가 fan 위) 면, 그 채널 후벽을
|
||||
* [post-win..post] 범위의 더 inward 한 후보(urine-wall ratio ≥ gate)로 교체.
|
||||
* (a) 분리형 peak, (b) shoulder.
|
||||
*/
|
||||
private fun applyInwardPost(
|
||||
results: MutableList<MethodDResult?>,
|
||||
angles: DoubleArray,
|
||||
win: Int = 16,
|
||||
ratioGate: Double = 1.15,
|
||||
frac: Double = 0.75,
|
||||
) {
|
||||
val det = (0 until kmin(4, results.size))
|
||||
.filter { results[it] != null }
|
||||
.sorted()
|
||||
if (det.size < 2) return
|
||||
|
||||
fun dOf(i: Int): Double {
|
||||
val r = results[i]!!
|
||||
val ang = if (i < angles.size) angles[i] else 0.0
|
||||
return (r.post - r.ant) * cos(Math.toRadians(ang))
|
||||
}
|
||||
val s = det.associateWith { dOf(it) * dOf(it) }
|
||||
val top = det[0]
|
||||
val topS = s[top]!!
|
||||
val maxS = s.values.max()
|
||||
val secondS = s[det[1]]!!
|
||||
if (topS < maxS || topS < secondS) return
|
||||
|
||||
val r = results[top]!!
|
||||
val p = r.post
|
||||
val ls = r.lowStart
|
||||
val le = r.lowEnd
|
||||
val lt = r.sgLight
|
||||
val hv = r.sgHeavy
|
||||
if (le <= ls || p - 2 <= ls + 3) return
|
||||
|
||||
val base = minInRange(hv, ls, le)
|
||||
if (base <= 0 || base.isNaN() || base.isInfinite()) return
|
||||
|
||||
val postRatio = if (p in lt.indices) lt[p] / base else 0.0
|
||||
val lo = maxOf(ls + 3, p - win)
|
||||
val cands = mutableListOf<Int>()
|
||||
for (i in lo until p - 2) {
|
||||
if (i !in lt.indices) continue
|
||||
if (lt[i] / base < ratioGate) continue
|
||||
|
||||
// (a) 분리형 peak: light local max, i~post 사이 valley, 깊은 peak 강도 frac 이상
|
||||
val isPeak = i - 1 in lt.indices && i + 1 in lt.indices
|
||||
&& lt[i] >= lt[i - 1] && lt[i] > lt[i + 1]
|
||||
if (isPeak) {
|
||||
var minLtIP = lt[i]
|
||||
for (j in i..p) if (j in lt.indices && lt[j] < minLtIP) minLtIP = lt[j]
|
||||
if (minLtIP < lt[i] * 0.97 && lt[i] >= postRatio * base * frac) {
|
||||
cands.add(i)
|
||||
continue
|
||||
}
|
||||
}
|
||||
// (b) shoulder: 3-샘플 plateau + 앞쪽 상승 + 뒤에 더 깊은 peak
|
||||
if (i + 3 <= p && i - 2 in lt.indices) {
|
||||
var maxWin = lt[i]; var minWin = lt[i]
|
||||
for (j in i until i + 3) {
|
||||
if (j in lt.indices) {
|
||||
if (lt[j] > maxWin) maxWin = lt[j]
|
||||
if (lt[j] < minWin) minWin = lt[j]
|
||||
}
|
||||
}
|
||||
val flat = (maxWin - minWin) < 0.02 * lt[i]
|
||||
val risingBefore = lt[i] > lt[i - 2] + 0.03 * base
|
||||
var maxAfter = lt[i + 3]
|
||||
for (j in i + 3..p) if (j in lt.indices && lt[j] > maxAfter) maxAfter = lt[j]
|
||||
val higherAfter = maxAfter > lt[i] * 1.03
|
||||
if (flat && risingBefore && higherAfter) {
|
||||
cands.add(i)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (cands.isEmpty()) return
|
||||
val newPost = cands.min()
|
||||
if (newPost < p - 3) {
|
||||
r.post = newPost
|
||||
r.postRefined = newPost.toDouble()
|
||||
r.urineLen = r.post - r.ant - 1
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// 공통 helpers
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/** z(i, idx) = (DELAY_OFFSET_MM + idx * DISTANCE_PER_SAMPLE) * cos(angle_i) — sample_to_ap_depth. */
|
||||
private fun zOf(angles: DoubleArray, ch: Int, idx: Int): Double {
|
||||
val ang = if (ch < angles.size) angles[ch] else 0.0
|
||||
return (PiezoHW.delayOffsetMm + idx * PiezoHW.distancePerSample) * cos(Math.toRadians(ang))
|
||||
}
|
||||
|
||||
/** numpy.median 동작 매칭 — 짝수 길이면 두 가운데 값의 평균. */
|
||||
private fun median(xs: List<Double>): Double {
|
||||
if (xs.isEmpty()) return 0.0
|
||||
val sorted = xs.sorted()
|
||||
val n = sorted.size
|
||||
return if (n % 2 == 1) sorted[n / 2]
|
||||
else (sorted[n / 2 - 1] + sorted[n / 2]) / 2.0
|
||||
}
|
||||
|
||||
/** arr[from..to] 최소 (numpy min 매칭). 유효 범위 밖은 skip. 없으면 0.0. */
|
||||
private fun minInRange(arr: DoubleArray, from: Int, to: Int): Double {
|
||||
var m = Double.POSITIVE_INFINITY
|
||||
val lo = maxOf(0, from)
|
||||
val hi = kmin(arr.size - 1, to)
|
||||
for (i in lo..hi) if (arr[i] < m) m = arr[i]
|
||||
return if (m.isFinite()) m else 0.0
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user