Compare commits
4 Commits
4f94811b49
...
7803e147a7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7803e147a7 | ||
|
|
13c1d417d6 | ||
|
|
35cbbb04c6 | ||
|
|
4d16825f7d |
@@ -24,6 +24,12 @@ def _bounds_from_args(args) -> SearchBounds:
|
||||
|
||||
def cmd_sweep(args) -> int:
|
||||
bounds = _bounds_from_args(args)
|
||||
if args.gpu:
|
||||
from gausse.gpu.batch_sweep import run_gpu_sweep
|
||||
|
||||
summary = run_gpu_sweep(Path(args.db), n_runs=args.n, bounds=bounds, seed=args.seed)
|
||||
print(f"GPU-путь backend={summary['backend']} (одноступ.)")
|
||||
else:
|
||||
summary = run_sweep(
|
||||
Path(args.db), n_runs=args.n, bounds=bounds, n_workers=args.workers, seed=args.seed
|
||||
)
|
||||
@@ -113,6 +119,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||
sweep_p.add_argument("--workers", type=int, default=None)
|
||||
sweep_p.add_argument("--seed", type=int, default=None)
|
||||
sweep_p.add_argument("--max-stages", type=int, default=None)
|
||||
sweep_p.add_argument("--gpu", action="store_true", help="GPU/cupy батч-путь (одноступ., быстрый)")
|
||||
sweep_p.set_defaults(func=cmd_sweep)
|
||||
|
||||
evolve_p = subparsers.add_parser("evolve", help="эволюционный поиск поверх базы прогонов")
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
"esr_ohm": 0.45,
|
||||
"max_current_a": 40.0,
|
||||
"price": 140.0,
|
||||
"source": "REAL цена: chipdip.ru, категория 'kondensator-400v' (https://www.chipdip.ru/catalog/popular/kondensator-400v); ESR ОЦЕНКА 0.3-0.6 Ом. max_current_a — ОЦЕНКА ИМПУЛЬСНОГО тока для одиночного coilgun-выстрела (не ripple из даташита), ограничено ESR/фольгой; в пачке параллельно (как у пользователя, 8шт) суммируется"
|
||||
"source": "REAL цена: chipdip.ru, категория 'kondensator-400v' (https://www.chipdip.ru/catalog/popular/kondensator-400v); ESR ОЦЕНКА 0.3-0.6 Ом. max_current_a — ОЦЕНКА ИМПУЛЬСНОГО тока для одиночного coilgun-выстрела (не ripple из даташита), ограничено ESR/фольгой; в пачке параллельно (как у пользователя, 8шт) суммируется",
|
||||
"url": "https://www.chipdip.ru/catalog/popular/kondensator-400v"
|
||||
},
|
||||
{
|
||||
"part_number": "cap-68uf-400v",
|
||||
@@ -15,7 +16,8 @@
|
||||
"esr_ohm": 0.3,
|
||||
"max_current_a": 60.0,
|
||||
"price": 440.0,
|
||||
"source": "REAL цена: chipdip.ru (https://www.chipdip.ru/catalog/popular/kondensator-400v); ESR ОЦЕНКА. max_current_a — ОЦЕНКА импульсного тока (одиночный выстрел), не ripple"
|
||||
"source": "REAL цена: chipdip.ru (https://www.chipdip.ru/catalog/popular/kondensator-400v); ESR ОЦЕНКА. max_current_a — ОЦЕНКА импульсного тока (одиночный выстрел), не ripple",
|
||||
"url": "https://www.chipdip.ru/catalog/popular/kondensator-400v"
|
||||
},
|
||||
{
|
||||
"part_number": "cap-180uf-400v",
|
||||
@@ -24,7 +26,8 @@
|
||||
"esr_ohm": 0.15,
|
||||
"max_current_a": 120.0,
|
||||
"price": 109.9,
|
||||
"source": "REAL цена: procontact74.ru, CHANG 180uF 400V 105C (https://procontact74.ru/01-elektronnyie-komponentyi-/41-kondensatory/411-elektroliticheskie-kondensatory/180uf-400v-105c-2530-411213/); ESR ОЦЕНКА. max_current_a — ОЦЕНКА импульсного тока, не ripple"
|
||||
"source": "REAL цена: procontact74.ru, CHANG 180uF 400V 105C (https://procontact74.ru/01-elektronnyie-komponentyi-/41-kondensatory/411-elektroliticheskie-kondensatory/180uf-400v-105c-2530-411213/); ESR ОЦЕНКА. max_current_a — ОЦЕНКА импульсного тока, не ripple",
|
||||
"url": "https://procontact74.ru/01-elektronnyie-komponentyi-/41-kondensatory/411-elektroliticheskie-kondensatory/180uf-400v-105c-2530-411213/"
|
||||
},
|
||||
{
|
||||
"part_number": "cap-470uf-400v",
|
||||
@@ -33,7 +36,8 @@
|
||||
"esr_ohm": 0.08,
|
||||
"max_current_a": 250.0,
|
||||
"price": 498.0,
|
||||
"source": "REAL цена: procontact74.ru, JCCON 470uF 400V 105C 30x45мм (https://procontact74.ru/01-elektronnyie-komponentyi-/41-kondensatory/411-elektroliticheskie-kondensatory/kondensator-elektroliticheskiy----470uf-400v-105-c-35-45-cd294-elzet/), проверено, 325.84₽ от 5шт; ESR ОЦЕНКА ~0.05-0.1 Ом. max_current_a — ОЦЕНКА импульсного тока (низкий ESR -> держит сотни А в импульсе), не ripple"
|
||||
"source": "REAL цена: procontact74.ru, JCCON 470uF 400V 105C 30x45мм (https://procontact74.ru/01-elektronnyie-komponentyi-/41-kondensatory/411-elektroliticheskie-kondensatory/kondensator-elektroliticheskiy----470uf-400v-105-c-35-45-cd294-elzet/), проверено, 325.84₽ от 5шт; ESR ОЦЕНКА ~0.05-0.1 Ом. max_current_a — ОЦЕНКА импульсного тока (низкий ESR -> держит сотни А в импульсе), не ripple",
|
||||
"url": "https://procontact74.ru/01-elektronnyie-komponentyi-/41-kondensatory/411-elektroliticheskie-kondensatory/kondensator-elektroliticheskiy----470uf-400v-105-c-35-45-cd294-elzet/"
|
||||
},
|
||||
{
|
||||
"part_number": "cap-1000uf-400v",
|
||||
@@ -42,7 +46,8 @@
|
||||
"esr_ohm": 0.06,
|
||||
"max_current_a": 350.0,
|
||||
"price": 700.0,
|
||||
"source": "ОЦЕНКА (низкая уверенность по цене): категория подтверждена на procontact74.ru/asenergi.ru, точная цена не подтверждена. max_current_a — ОЦЕНКА импульсного тока для coilgun, не ripple"
|
||||
"source": "ОЦЕНКА (низкая уверенность по цене): категория подтверждена на procontact74.ru/asenergi.ru, точная цена не подтверждена. max_current_a — ОЦЕНКА импульсного тока для coilgun, не ripple",
|
||||
"url": "https://procontact74.ru/01-elektronnyie-komponentyi-/41-kondensatory/411-elektroliticheskie-kondensatory/"
|
||||
},
|
||||
{
|
||||
"part_number": "cap-2200uf-400v",
|
||||
@@ -51,6 +56,7 @@
|
||||
"esr_ohm": 0.05,
|
||||
"max_current_a": 450.0,
|
||||
"price": 1400.0,
|
||||
"source": "ОЦЕНКА (низкая уверенность по цене): категория подтверждена на asenergi.ru (https://asenergi.ru/catalog/kondensatory-elektroliticheskie/2200mkf.html). max_current_a — ОЦЕНКА импульсного тока для coilgun, не ripple"
|
||||
"source": "ОЦЕНКА (низкая уверенность по цене): категория подтверждена на asenergi.ru (https://asenergi.ru/catalog/kondensatory-elektroliticheskie/2200mkf.html). max_current_a — ОЦЕНКА импульсного тока для coilgun, не ripple",
|
||||
"url": "https://asenergi.ru/catalog/kondensatory-elektroliticheskie/2200mkf.html"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
"mu_r": 550.0,
|
||||
"b_sat_tesla": 1.9,
|
||||
"price_per_kg": 55.0,
|
||||
"source": "Цена REAL: горячекатаный круг Ст3, ros-met.com/metallurg-moskva.ru, ~49-60₽/кг (одна из позиций ~51.4₽/кг за тонну); плотность/μr/B_sat — ОЦЕНКА (стандартные справочные значения для конструкционной стали, не найдено источника с числами конкретно для Ст3)"
|
||||
"resistivity_ohm_m": 1.6e-07,
|
||||
"source": "Цена REAL: горячекатаный круг Ст3, ros-met.com/metallurg-moskva.ru, ~49-60₽/кг (одна из позиций ~51.4₽/кг за тонну); плотность/μr/B_sat/resistivity — ОЦЕНКА (справочные значения для конструкционной стали; удельное сопротивление ~1.6e-7 Ом·м)",
|
||||
"url": "https://ros-met.com/krug-stalnoy-kupit/"
|
||||
},
|
||||
{
|
||||
"name": "Сталь 10 (низкоуглеродистая)",
|
||||
@@ -13,7 +15,9 @@
|
||||
"mu_r": 1500.0,
|
||||
"b_sat_tesla": 2.05,
|
||||
"price_per_kg": 55.0,
|
||||
"source": "ОЦЕНКА по всем полям: отдельного розничного объявления на 'Сталь 10' не найдено, цена взята той же полосы, что и Ст3 (~50-70₽/кг); μr выше, чем у Ст3, из-за более высокой чистоты низкоуглеродистой стали — справочная оценка, не измерение"
|
||||
"resistivity_ohm_m": 1.4e-07,
|
||||
"source": "ОЦЕНКА по всем полям: отдельного розничного объявления на 'Сталь 10' не найдено, цена той же полосы, что и Ст3 (~50-70₽/кг); μr выше из-за чистоты; удельное сопротивление ~1.4e-7 Ом·м — справочная оценка",
|
||||
"url": "https://ros-met.com/krug-stalnoy-kupit/"
|
||||
},
|
||||
{
|
||||
"name": "Армко-железо (чистое железо)",
|
||||
@@ -21,6 +25,8 @@
|
||||
"mu_r": 3500.0,
|
||||
"b_sat_tesla": 2.15,
|
||||
"price_per_kg": 160.0,
|
||||
"source": "ОЦЕНКА по всем полям: специализированный товар, не найдено самостоятельной розничной цены; цена оценена как ~3x от Ст3 на основе общего указания '2-4x дороже обычной стали'; μr/B_sat — стандартные справочные значения для хорошо отожжённого чистого железа"
|
||||
"resistivity_ohm_m": 1e-07,
|
||||
"source": "ОЦЕНКА по всем полям: специализированный товар, розничной цены не найдено; цена ~3x от Ст3; μr/B_sat — справочные для отожжённого чистого железа; удельное сопротивление ~1.0e-7 Ом·м (чище -> ниже)",
|
||||
"url": "https://ros-met.com/krug-stalnoy-kupit/"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -4,14 +4,16 @@
|
||||
"kind": "hall",
|
||||
"propagation_delay_ns": 5000.0,
|
||||
"price": 19.9,
|
||||
"source": "REAL: procontact74.ru (https://procontact74.ru/01-elektronnyie-komponentyi-/34-datchiki/datchik-holla-a3144e-34111/), 19.90₽ (6.48₽ от 200шт); время срабатывания на странице не указано, ОЦЕНКА ~5мкс (середина типового диапазона 3-7мкс для А3144-класса)"
|
||||
"source": "REAL: procontact74.ru (https://procontact74.ru/01-elektronnyie-komponentyi-/34-datchiki/datchik-holla-a3144e-34111/), 19.90₽ (6.48₽ от 200шт); время срабатывания на странице не указано, ОЦЕНКА ~5мкс (середина типового диапазона 3-7мкс для А3144-класса)",
|
||||
"url": "https://procontact74.ru/01-elektronnyie-komponentyi-/34-datchiki/datchik-holla-a3144e-34111/"
|
||||
},
|
||||
{
|
||||
"part_number": "TCST2103",
|
||||
"kind": "optical",
|
||||
"propagation_delay_ns": 9000.0,
|
||||
"price": 220.0,
|
||||
"source": "REAL: chipdip.ru (https://www.chipdip.ru/product/tcst2103-optopara-shirokogo-naznacheniya-4-pin-vishay-2693015011), 220₽ (161₽ от 200шт); время включения/выключения 8-10мкс — REAL с даташита Vishay, взята середина диапазона"
|
||||
"source": "REAL: chipdip.ru (https://www.chipdip.ru/product/tcst2103-optopara-shirokogo-naznacheniya-4-pin-vishay-2693015011), 220₽ (161₽ от 200шт); время включения/выключения 8-10мкс — REAL с даташита Vishay, взята середина диапазона",
|
||||
"url": "https://www.chipdip.ru/product/tcst2103-optopara-shirokogo-naznacheniya-4-pin-vishay-2693015011"
|
||||
},
|
||||
{
|
||||
"part_number": "inductive-pickup-lm393",
|
||||
@@ -20,6 +22,7 @@
|
||||
"price": 15.0,
|
||||
"source": "ОЦЕНКА: не готовый компонент, а самодельная схема — виток(-и) провода (цена уже учтена в wires.json) + компаратор LM393 (~15₽, типовая розничная цена); время задержки 1.3мкс — реальный паспортный параметр LM393, но чувствительность/порог ниже — грубая оценка, не датащит-значение",
|
||||
"sensitivity_v_per_mps": 0.01,
|
||||
"threshold_v": 0.05
|
||||
"threshold_v": 0.05,
|
||||
"url": "https://www.chipdip.ru/search?searchtext=LM393"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
"on_voltage_drop_v": 1.5,
|
||||
"turn_on_time_ns": 10000.0,
|
||||
"price": 140.0,
|
||||
"source": "REAL: КУ202Н, chipdip.ru (https://www.chipdip.ru/product/ku202n) 140₽ / procontact74.ru (https://procontact74.ru/44-tiristory/ku202n-tiristor-10a-400v-44011/) 189₽; Von=1.5В с даташита; время включения ОЦЕНКА ~10мкс (типично для тиристора этого класса, точное значение в листинге не приведено)"
|
||||
"source": "REAL: КУ202Н, chipdip.ru (https://www.chipdip.ru/product/ku202n) 140₽ / procontact74.ru (https://procontact74.ru/44-tiristory/ku202n-tiristor-10a-400v-44011/) 189₽; Von=1.5В с даташита; время включения ОЦЕНКА ~10мкс (типично для тиристора этого класса, точное значение в листинге не приведено)",
|
||||
"url": "https://www.chipdip.ru/product/ku202n"
|
||||
},
|
||||
{
|
||||
"part_number": "BT151-650R",
|
||||
@@ -19,7 +20,8 @@
|
||||
"on_voltage_drop_v": 1.7,
|
||||
"turn_on_time_ns": 1500.0,
|
||||
"price": 190.0,
|
||||
"source": "REAL: chipdip.ru (https://www.chipdip.ru/product/bt151-650r), ~180-200₽ (BT151-600R вариант подтверждён на 180₽); Von и время включения ОЦЕНКА (типично для тиристора этого класса, не указаны продавцом)"
|
||||
"source": "REAL: chipdip.ru (https://www.chipdip.ru/product/bt151-650r), ~180-200₽ (BT151-600R вариант подтверждён на 180₽); Von и время включения ОЦЕНКА (типично для тиристора этого класса, не указаны продавцом)",
|
||||
"url": "https://www.chipdip.ru/product/bt151-650r"
|
||||
},
|
||||
{
|
||||
"part_number": "IRFP254PBF",
|
||||
@@ -30,7 +32,8 @@
|
||||
"on_voltage_drop_v": null,
|
||||
"turn_on_time_ns": 30.0,
|
||||
"price": 39.9,
|
||||
"source": "REAL: procontact74.ru (https://procontact74.ru/01-elektronnyie-komponentyi-/43-tranzistory/polevyie--mosfet-/58406-tranzistorirfp254pbfmosfetnch250v23ato247acoriginalbydemontajnogi10mm/); ВНИМАНИЕ: демонтированная (Б/У) деталь, не новая партия; время включения ОЦЕНКА ~30нс (типично для этого класса MOSFET)"
|
||||
"source": "REAL: procontact74.ru (https://procontact74.ru/01-elektronnyie-komponentyi-/43-tranzistory/polevyie--mosfet-/58406-tranzistorirfp254pbfmosfetnch250v23ato247acoriginalbydemontajnogi10mm/); ВНИМАНИЕ: демонтированная (Б/У) деталь, не новая партия; время включения ОЦЕНКА ~30нс (типично для этого класса MOSFET)",
|
||||
"url": "https://procontact74.ru/01-elektronnyie-komponentyi-/43-tranzistory/polevyie--mosfet-/58406-tranzistorirfp254pbfmosfetnch250v23ato247acoriginalbydemontajnogi10mm/"
|
||||
},
|
||||
{
|
||||
"part_number": "IRFP250PBF",
|
||||
@@ -41,7 +44,8 @@
|
||||
"on_voltage_drop_v": null,
|
||||
"turn_on_time_ns": 35.0,
|
||||
"price": 320.0,
|
||||
"source": "REAL: chipdip.ru (https://www.chipdip.ru/product/irfp250), новая деталь, 320₽ (276₽ от 15шт, 245₽ от 150шт); Rds(on)=0.085Ом при 18А/10В с даташита; время включения ОЦЕНКА"
|
||||
"source": "REAL: chipdip.ru (https://www.chipdip.ru/product/irfp250), новая деталь, 320₽ (276₽ от 15шт, 245₽ от 150шт); Rds(on)=0.085Ом при 18А/10В с даташита; время включения ОЦЕНКА",
|
||||
"url": "https://www.chipdip.ru/product/irfp250"
|
||||
},
|
||||
{
|
||||
"part_number": "IRFP4468PBF",
|
||||
@@ -52,7 +56,8 @@
|
||||
"on_voltage_drop_v": null,
|
||||
"turn_on_time_ns": 40.0,
|
||||
"price": 610.0,
|
||||
"source": "REAL: chipdip.ru (https://www.chipdip.ru/product/irfp4468pbf), 610₽ (534₽ от 15шт); паспортный максимум 290А при 25°C, взято консервативное практическое значение 180А; Rds(on)=0.0026Ом при 180А/10В с даташита; время включения ОЦЕНКА"
|
||||
"source": "REAL: chipdip.ru (https://www.chipdip.ru/product/irfp4468pbf), 610₽ (534₽ от 15шт); паспортный максимум 290А при 25°C, взято консервативное практическое значение 180А; Rds(on)=0.0026Ом при 180А/10В с даташита; время включения ОЦЕНКА",
|
||||
"url": "https://www.chipdip.ru/product/irfp4468pbf"
|
||||
},
|
||||
{
|
||||
"part_number": "IRG4PC50F",
|
||||
@@ -63,6 +68,7 @@
|
||||
"on_voltage_drop_v": 1.8,
|
||||
"turn_on_time_ns": 300.0,
|
||||
"price": 504.0,
|
||||
"source": "REAL: chipdip.ru (https://www.chipdip.ru/product/irg4pc50f-8048990896), ~504₽; Vce(sat) и время включения ОЦЕНКА (типично для этого семейства IGBT, не подтверждено со страницы товара)"
|
||||
"source": "REAL: chipdip.ru (https://www.chipdip.ru/product/irg4pc50f-8048990896), ~504₽; Vce(sat) и время включения ОЦЕНКА (типично для этого семейства IGBT, не подтверждено со страницы товара)",
|
||||
"url": "https://www.chipdip.ru/product/irg4pc50f-8048990896"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
"resistivity_ohm_m": 1.68e-08,
|
||||
"max_current_a": 0.35,
|
||||
"price_per_m": 3.5,
|
||||
"source": "REAL: chipdip.ru, ПЭТВ-2 0.3мм, 1050₽/200г-катушка ~300м (https://www.chipdip.ru/product/petv-2-d-0.3-mm-provod-emalirovannyy-obmotochnyy-300m-dlina-9000272019)"
|
||||
"source": "REAL: chipdip.ru, ПЭТВ-2 0.3мм, 1050₽/200г-катушка ~300м (https://www.chipdip.ru/product/petv-2-d-0.3-mm-provod-emalirovannyy-obmotochnyy-300m-dlina-9000272019)",
|
||||
"url": "https://www.chipdip.ru/product/petv-2-d-0.3-mm-provod-emalirovannyy-obmotochnyy-300m-dlina-9000272019"
|
||||
},
|
||||
{
|
||||
"part_id": "cu-petv2-0.71mm",
|
||||
@@ -17,7 +18,8 @@
|
||||
"resistivity_ohm_m": 1.68e-08,
|
||||
"max_current_a": 1.98,
|
||||
"price_per_m": 16.2,
|
||||
"source": "REAL: chipdip.ru, ПЭТВ-2 0.71мм, 890₽/200г-катушка ~55м (https://www.chipdip.ru/product/petv-2-d-0.71-mm)"
|
||||
"source": "REAL: chipdip.ru, ПЭТВ-2 0.71мм, 890₽/200г-катушка ~55м (https://www.chipdip.ru/product/petv-2-d-0.71-mm)",
|
||||
"url": "https://www.chipdip.ru/product/petv-2-d-0.71-mm"
|
||||
},
|
||||
{
|
||||
"part_id": "cu-petv2-1.0mm",
|
||||
@@ -27,7 +29,8 @@
|
||||
"resistivity_ohm_m": 1.68e-08,
|
||||
"max_current_a": 3.93,
|
||||
"price_per_m": 8.445,
|
||||
"source": "REAL: chipdip.ru, ПЭТВ-2 1.0мм, 1200₽/кг (https://www.chipdip.ru/product0/9000289279); ₽/м пересчитано нами из ₽/кг"
|
||||
"source": "REAL: chipdip.ru, ПЭТВ-2 1.0мм, 1200₽/кг (https://www.chipdip.ru/product0/9000289279); ₽/м пересчитано нами из ₽/кг",
|
||||
"url": "https://www.chipdip.ru/product0/9000289279"
|
||||
},
|
||||
{
|
||||
"part_id": "cu-petv2-1.4mm",
|
||||
@@ -37,7 +40,8 @@
|
||||
"resistivity_ohm_m": 1.68e-08,
|
||||
"max_current_a": 7.7,
|
||||
"price_per_m": 45.266,
|
||||
"source": "REAL: chipdip.ru, ПЭТВ-2 1.4мм, 3610₽/1.1кг-упаковка (https://www.chipdip.ru/product0/8004178560); ₽/м пересчитано нами из ₽/кг"
|
||||
"source": "REAL: chipdip.ru, ПЭТВ-2 1.4мм, 3610₽/1.1кг-упаковка (https://www.chipdip.ru/product0/8004178560); ₽/м пересчитано нами из ₽/кг",
|
||||
"url": "https://www.chipdip.ru/product0/8004178560"
|
||||
},
|
||||
{
|
||||
"part_id": "cu-petv2-0.8mm",
|
||||
@@ -47,7 +51,8 @@
|
||||
"resistivity_ohm_m": 1.68e-08,
|
||||
"max_current_a": 2.51,
|
||||
"price_per_m": 11.259,
|
||||
"source": "ОЦЕНКА: товар реален на chipdip.ru (https://www.chipdip.ru/product0/8004178423), но точная цена не подтверждена при парсинге; 2500₽/кг интерполировано между подтверждёнными 0.71мм (~4566₽/кг) и 1.0мм (1200₽/кг)"
|
||||
"source": "ОЦЕНКА: товар реален на chipdip.ru (https://www.chipdip.ru/product0/8004178423), но точная цена не подтверждена при парсинге; 2500₽/кг интерполировано между подтверждёнными 0.71мм (~4566₽/кг) и 1.0мм (1200₽/кг)",
|
||||
"url": "https://www.chipdip.ru/product0/8004178423"
|
||||
},
|
||||
{
|
||||
"part_id": "cu-petv2-2.0mm",
|
||||
@@ -57,7 +62,8 @@
|
||||
"resistivity_ohm_m": 1.68e-08,
|
||||
"max_current_a": 15.71,
|
||||
"price_per_m": 30.964,
|
||||
"source": "ОЦЕНКА: chipdip.ru (https://www.chipdip.ru/product0/9000289280), цена частично 'по запросу', взята середина наблюдаемого диапазона 1000-1200₽/кг"
|
||||
"source": "ОЦЕНКА: chipdip.ru (https://www.chipdip.ru/product0/9000289280), цена частично 'по запросу', взята середина наблюдаемого диапазона 1000-1200₽/кг",
|
||||
"url": "https://www.chipdip.ru/product0/9000289280"
|
||||
},
|
||||
{
|
||||
"part_id": "al-0.3mm",
|
||||
@@ -67,7 +73,8 @@
|
||||
"resistivity_ohm_m": 2.82e-08,
|
||||
"max_current_a": 0.28,
|
||||
"price_per_m": 1.4,
|
||||
"source": "ОЦЕНКА: алюминиевый эмальпровод в рознице РФ практически не продаётся поштучно (cable.ru и др. требуют запрос цены, см. https://cable.ru/cable/kabel-alyuminij_obmotochnyj_v_razdel-9.php); цена оценена как 0.4x от цены медного провода того же сечения (типовое соотношение стоимости сырья Cu/Al)"
|
||||
"source": "ОЦЕНКА: алюминиевый эмальпровод в рознице РФ практически не продаётся поштучно (cable.ru и др. требуют запрос цены, см. https://cable.ru/cable/kabel-alyuminij_obmotochnyj_v_razdel-9.php); цена оценена как 0.4x от цены медного провода того же сечения (типовое соотношение стоимости сырья Cu/Al)",
|
||||
"url": "https://cable.ru/cable/kabel-alyuminij_obmotochnyj_v_razdel-9.php"
|
||||
},
|
||||
{
|
||||
"part_id": "al-0.71mm",
|
||||
@@ -77,7 +84,8 @@
|
||||
"resistivity_ohm_m": 2.82e-08,
|
||||
"max_current_a": 1.58,
|
||||
"price_per_m": 6.48,
|
||||
"source": "ОЦЕНКА: алюминиевый эмальпровод в рознице РФ практически не продаётся поштучно (cable.ru и др. требуют запрос цены, см. https://cable.ru/cable/kabel-alyuminij_obmotochnyj_v_razdel-9.php); цена оценена как 0.4x от цены медного провода того же сечения (типовое соотношение стоимости сырья Cu/Al)"
|
||||
"source": "ОЦЕНКА: алюминиевый эмальпровод в рознице РФ практически не продаётся поштучно (cable.ru и др. требуют запрос цены, см. https://cable.ru/cable/kabel-alyuminij_obmotochnyj_v_razdel-9.php); цена оценена как 0.4x от цены медного провода того же сечения (типовое соотношение стоимости сырья Cu/Al)",
|
||||
"url": "https://cable.ru/cable/kabel-alyuminij_obmotochnyj_v_razdel-9.php"
|
||||
},
|
||||
{
|
||||
"part_id": "al-1.0mm",
|
||||
@@ -87,7 +95,8 @@
|
||||
"resistivity_ohm_m": 2.82e-08,
|
||||
"max_current_a": 3.14,
|
||||
"price_per_m": 3.378,
|
||||
"source": "ОЦЕНКА: алюминиевый эмальпровод в рознице РФ практически не продаётся поштучно (cable.ru и др. требуют запрос цены, см. https://cable.ru/cable/kabel-alyuminij_obmotochnyj_v_razdel-9.php); цена оценена как 0.4x от цены медного провода того же сечения (типовое соотношение стоимости сырья Cu/Al)"
|
||||
"source": "ОЦЕНКА: алюминиевый эмальпровод в рознице РФ практически не продаётся поштучно (cable.ru и др. требуют запрос цены, см. https://cable.ru/cable/kabel-alyuminij_obmotochnyj_v_razdel-9.php); цена оценена как 0.4x от цены медного провода того же сечения (типовое соотношение стоимости сырья Cu/Al)",
|
||||
"url": "https://cable.ru/cable/kabel-alyuminij_obmotochnyj_v_razdel-9.php"
|
||||
},
|
||||
{
|
||||
"part_id": "al-1.4mm",
|
||||
@@ -97,6 +106,7 @@
|
||||
"resistivity_ohm_m": 2.82e-08,
|
||||
"max_current_a": 6.16,
|
||||
"price_per_m": 18.106,
|
||||
"source": "ОЦЕНКА: алюминиевый эмальпровод в рознице РФ практически не продаётся поштучно (cable.ru и др. требуют запрос цены, см. https://cable.ru/cable/kabel-alyuminij_obmotochnyj_v_razdel-9.php); цена оценена как 0.4x от цены медного провода того же сечения (типовое соотношение стоимости сырья Cu/Al)"
|
||||
"source": "ОЦЕНКА: алюминиевый эмальпровод в рознице РФ практически не продаётся поштучно (cable.ru и др. требуют запрос цены, см. https://cable.ru/cable/kabel-alyuminij_obmotochnyj_v_razdel-9.php); цена оценена как 0.4x от цены медного провода того же сечения (типовое соотношение стоимости сырья Cu/Al)",
|
||||
"url": "https://cable.ru/cable/kabel-alyuminij_obmotochnyj_v_razdel-9.php"
|
||||
}
|
||||
]
|
||||
@@ -14,6 +14,7 @@ class WireSpec:
|
||||
max_current_a: float
|
||||
price_per_m: float
|
||||
source: str
|
||||
url: str | None = None # прямая ссылка на магазин, где купить
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -25,6 +26,7 @@ class CapacitorSpec:
|
||||
max_current_a: float
|
||||
price: float
|
||||
source: str
|
||||
url: str | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -46,6 +48,7 @@ class CapacitorBank:
|
||||
esr_ohm: float
|
||||
max_current_a: float
|
||||
price: float
|
||||
url: str | None = None
|
||||
|
||||
@property
|
||||
def count(self) -> int:
|
||||
@@ -64,6 +67,7 @@ class CapacitorBank:
|
||||
esr_ohm=spec.esr_ohm * n_series / n_parallel,
|
||||
max_current_a=spec.max_current_a * n_parallel,
|
||||
price=spec.price * n_series * n_parallel,
|
||||
url=spec.url,
|
||||
)
|
||||
|
||||
|
||||
@@ -78,6 +82,7 @@ class SwitchSpec:
|
||||
turn_on_time_ns: float
|
||||
price: float
|
||||
source: str
|
||||
url: str | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -89,6 +94,7 @@ class SensorSpec:
|
||||
source: str
|
||||
sensitivity_v_per_mps: float | None = None
|
||||
threshold_v: float | None = None
|
||||
url: str | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -99,3 +105,7 @@ class ProjectileMaterialSpec:
|
||||
b_sat_tesla: float
|
||||
price_per_kg: float
|
||||
source: str
|
||||
# электрическое удельное сопротивление — для расчёта вихревых потерь в снаряде
|
||||
# (сплошной проводник в импульсном поле). Дефолт ~ конструкционная сталь.
|
||||
resistivity_ohm_m: float = 1.6e-7
|
||||
url: str | None = None
|
||||
|
||||
@@ -34,6 +34,7 @@ class BatchDischargeParams:
|
||||
slug_length_m: "any"
|
||||
smoothing_width_m: "any"
|
||||
i_sat_a: "any" # ток насыщения; NO_SATURATION_I_SAT где железа нет
|
||||
r_eddy_coeff_ohm: "any" # вихревое сопротивление снаряда (× overlap)
|
||||
|
||||
|
||||
def params_from_models(xp, models, circuit_params) -> BatchDischargeParams:
|
||||
@@ -60,6 +61,7 @@ def params_from_models(xp, models, circuit_params) -> BatchDischargeParams:
|
||||
slug_length_m=col([m.slug_length_m for m in models]),
|
||||
smoothing_width_m=col([m.smoothing_width_m for m in models]),
|
||||
i_sat_a=col(i_sat),
|
||||
r_eddy_coeff_ohm=col([getattr(c, "r_eddy_coeff_ohm", 0.0) for c in circuit_params]),
|
||||
)
|
||||
|
||||
|
||||
@@ -68,13 +70,25 @@ def _ln_cosh(xp, z):
|
||||
return az + xp.log1p(xp.exp(-2 * az)) - xp.log(xp.asarray(2.0))
|
||||
|
||||
|
||||
def _derivatives(xp, q, i, x, v, p: BatchDischargeParams):
|
||||
def _overlap(xp, x, p: BatchDischargeParams):
|
||||
half_span = (p.coil_length_m + p.slug_length_m) / 2
|
||||
w = p.smoothing_width_m
|
||||
s1 = 0.5 * (1 + xp.tanh((x + half_span) / w / 2))
|
||||
s2 = 0.5 * (1 + xp.tanh((half_span - x) / w / 2))
|
||||
overlap = s1 * s2
|
||||
d_overlap = (s1 * s2 / w) * (s2 - s1)
|
||||
return s1 * s2, (s1 * s2 / w) * (s2 - s1)
|
||||
|
||||
|
||||
def _magnetic_energy(xp, x, i, p: BatchDischargeParams):
|
||||
"""W = ½L_air·I² + L_iron·overlap·(I·g − G) — точная (с насыщением) энергия поля."""
|
||||
overlap, _ = _overlap(xp, x, p)
|
||||
z = i / p.i_sat_a
|
||||
g = p.i_sat_a * xp.tanh(z)
|
||||
g_integral = p.i_sat_a**2 * _ln_cosh(xp, z)
|
||||
return 0.5 * p.l_air_h * i**2 + p.l_iron_coeff * overlap * (i * g - g_integral)
|
||||
|
||||
|
||||
def _derivatives(xp, q, i, x, v, p: BatchDischargeParams):
|
||||
overlap, d_overlap = _overlap(xp, x, p)
|
||||
|
||||
z = i / p.i_sat_a
|
||||
tanhz = xp.tanh(z)
|
||||
@@ -86,9 +100,10 @@ def _derivatives(xp, q, i, x, v, p: BatchDischargeParams):
|
||||
dlambda_dx = p.l_iron_coeff * d_overlap * g
|
||||
force = p.l_iron_coeff * d_overlap * g_integral
|
||||
|
||||
r_eff = p.r_total_ohm + p.r_eddy_coeff_ohm * overlap # вихревые потери снаряда × overlap
|
||||
v_c = q / p.capacitance_f
|
||||
d_q = -i
|
||||
d_i = (v_c - i * p.r_total_ohm - dlambda_dx * v) / dlambda_di
|
||||
d_i = (v_c - i * r_eff - dlambda_dx * v) / dlambda_di
|
||||
d_v = force / p.mass_kg
|
||||
d_x = v
|
||||
return d_q, d_i, d_x, d_v
|
||||
@@ -115,12 +130,15 @@ def integrate_batch_discharge(
|
||||
v = xp.array(v0, dtype=xp.float64)
|
||||
|
||||
done = xp.zeros(n, dtype=bool)
|
||||
committed = xp.zeros(n, dtype=bool) # реально скоммутировал (валидный обрыв), а не «взорвался»
|
||||
past_peak = xp.zeros(n, dtype=bool) # ток уже прошёл пик и начал спадать
|
||||
exit_v = xp.array(v0, dtype=xp.float64)
|
||||
exit_x = xp.array(x0, dtype=xp.float64)
|
||||
exit_q = xp.array(q0, dtype=xp.float64)
|
||||
peak_current = xp.zeros(n, dtype=xp.float64)
|
||||
energy_diss = xp.zeros(n, dtype=xp.float64)
|
||||
|
||||
_old_err = xp.seterr(all="ignore") if hasattr(xp, "seterr") else None # стиффные конфиги переполняют fixed-step
|
||||
for _ in range(max_steps):
|
||||
active = ~done
|
||||
if not bool(xp.any(active)):
|
||||
@@ -139,27 +157,48 @@ def integrate_batch_discharge(
|
||||
x_new = x_old + dt / 6 * (k1[2] + 2 * k2[2] + 2 * k3[2] + k4[2])
|
||||
v_new = v_old + dt / 6 * (k1[3] + 2 * k2[3] + 2 * k3[3] + k4[3])
|
||||
|
||||
# потери ∫I²R dt (трапеция) и пиковый ток — только для активных
|
||||
step_diss = 0.5 * (i_old**2 + i_new**2) * params.r_total_ohm * dt
|
||||
# потери I²·R_eff (с вихревыми × overlap) и пиковый ток — только для активных
|
||||
overlap_old, _ = _overlap(xp, x_old, params)
|
||||
overlap_new, _ = _overlap(xp, x_new, params)
|
||||
r_eff_old = params.r_total_ohm + params.r_eddy_coeff_ohm * overlap_old
|
||||
r_eff_new = params.r_total_ohm + params.r_eddy_coeff_ohm * overlap_new
|
||||
step_diss = 0.5 * (i_old**2 * r_eff_old + i_new**2 * r_eff_new) * dt
|
||||
energy_diss = energy_diss + xp.where(active, step_diss, 0.0)
|
||||
peak_current = xp.maximum(peak_current, xp.where(active, xp.abs(i_new), 0.0))
|
||||
past_peak = past_peak | (active & (i_old > 1.0) & (i_new < i_old))
|
||||
|
||||
# нуль тока: ток был положительным и стал <=0 -> разряд закончился.
|
||||
# exit-значения берём интерполяцией СТАРОГО и НОВОГО состояния в точке I=0.
|
||||
# ОДИН импульс: обрыв на нуле тока ИЛИ на первом локальном минимуме
|
||||
# (снаряд начал подкачивать ток обратно). Что раньше.
|
||||
crossed = active & (i_old > 0) & (i_new <= 0)
|
||||
local_min = active & past_peak & (i_new > i_old) & ~crossed
|
||||
cut = crossed | local_min
|
||||
# zero-crossing: интерполяция к I=0; local_min: берём состояние минимума (old)
|
||||
frac = xp.where(crossed, i_old / (i_old - i_new + 1e-30), 0.0)
|
||||
exit_v = xp.where(crossed, _interp(v_old, v_new, frac), exit_v)
|
||||
exit_x = xp.where(crossed, _interp(x_old, x_new, frac), exit_x)
|
||||
exit_q = xp.where(crossed, _interp(q_old, q_new, frac), exit_q)
|
||||
done = done | crossed
|
||||
exit_v = xp.where(local_min, v_old, exit_v)
|
||||
exit_x = xp.where(local_min, x_old, exit_x)
|
||||
exit_q = xp.where(local_min, q_old, exit_q)
|
||||
# остаточная энергия катушки при обрыве на минимуме -> в потери (freewheel)
|
||||
residual = _magnetic_energy(xp, x_old, i_old, params)
|
||||
energy_diss = energy_diss + xp.where(local_min, residual, 0.0)
|
||||
committed = committed | cut # валидная коммутация
|
||||
|
||||
# продвигаем только ещё активные конфигурации
|
||||
q = xp.where(active, q_new, q_old)
|
||||
i = xp.where(active, i_new, i_old)
|
||||
x = xp.where(active, x_new, x_old)
|
||||
v = xp.where(active, v_new, v_old)
|
||||
# стиффный конфиг «взорвал» fixed-step (inf/nan) -> стоп, НЕ реализуем
|
||||
blew_up = active & ~cut & ~xp.isfinite(i_new)
|
||||
done = done | cut | blew_up
|
||||
|
||||
feasible = done
|
||||
# продвигаем только ещё активные и не взорвавшиеся конфигурации
|
||||
advance = active & ~blew_up
|
||||
q = xp.where(advance, q_new, q_old)
|
||||
i = xp.where(advance, i_new, i_old)
|
||||
x = xp.where(advance, x_new, x_old)
|
||||
v = xp.where(advance, v_new, v_old)
|
||||
|
||||
if _old_err is not None and hasattr(xp, "seterr"):
|
||||
xp.seterr(**_old_err)
|
||||
feasible = committed # реализуемо = реально скоммутировал, а не done-по-переполнению
|
||||
return {
|
||||
"exit_v": exit_v,
|
||||
"exit_x": exit_x,
|
||||
|
||||
176
src/gausse/gpu/batch_sweep.py
Normal file
176
src/gausse/gpu/batch_sweep.py
Normal file
@@ -0,0 +1,176 @@
|
||||
"""GPU/CPU батч-sweep одноступенчатых конфигураций.
|
||||
|
||||
Векторизует самую дорогую часть (разряд) сразу по N конфигурациям через
|
||||
`integrate_batch_discharge` (numpy CPU / cupy GPU). Настройка каждой
|
||||
конфигурации (decode + build_stage_physics) — обычный питон-цикл (быстрый),
|
||||
интегрирование — один батч.
|
||||
|
||||
Ограничения (честно): только ОДНА ступень; триггер датчика берётся
|
||||
аналитически при постоянной скорости подлёта (x_fire ≈ x_sensor + v·delay,
|
||||
одинаково для оптики/Холла/индукции), для индукционного датчика применяется
|
||||
та же проверка порога, что в CPU-пути. Полный конвейер (много ступеней,
|
||||
точный триггер) остаётся на CPU sweep/evolve. Числа физики идентичны CPU —
|
||||
через общий `build_stage_physics` и валидированный батч-интегратор.
|
||||
"""
|
||||
|
||||
import json
|
||||
import math
|
||||
import random
|
||||
import uuid
|
||||
from dataclasses import replace
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
from gausse.components.database import ComponentDatabase
|
||||
from gausse.gpu.backend import get_backend, to_cpu
|
||||
from gausse.gpu.batch_integrator import integrate_batch_discharge, params_from_models
|
||||
from gausse.optim.objective import MODEL_VERSION, build_detail
|
||||
from gausse.optim.progress_log import ProgressLogger, default_log_path
|
||||
from gausse.optim.search_space import SearchBounds, decode, genome_to_dict, sample_genome
|
||||
from gausse.physics.constants import SWITCH_SURGE_FACTOR
|
||||
from gausse.sim.coilgun import CoilgunResult, StageOutcome
|
||||
from gausse.sim.stage import StageResult, build_stage_physics
|
||||
from gausse.storage.database import insert_runs, open_connection
|
||||
from gausse.storage.schema import RunRecord
|
||||
|
||||
|
||||
def _prepare(genome, db, bounds):
|
||||
"""Готовит один одноступенчатый прогон: физика + аналитический fire-state.
|
||||
|
||||
Возвращает dict с моделью/параметрами/начальным состоянием или
|
||||
(None, причина) если конфигурация нереализуема ещё до интегрирования.
|
||||
"""
|
||||
config, initial_x_m, initial_v_mps = decode(genome, db, bounds)
|
||||
stage = config.stages[0]
|
||||
proj = config.projectile
|
||||
phys = build_stage_physics(stage, proj)
|
||||
|
||||
x_sensor = -stage.sensor_to_coil_distance_m
|
||||
fire_delay = (stage.sensor.propagation_delay_ns + stage.switch.turn_on_time_ns) * 1e-9
|
||||
|
||||
if stage.sensor.kind == "inductive":
|
||||
peak_v = stage.sensor.sensitivity_v_per_mps * abs(initial_v_mps)
|
||||
if peak_v < (stage.sensor.threshold_v or 0.0):
|
||||
return None, ("инд. датчик: сигнал ниже порога", config, initial_x_m, initial_v_mps)
|
||||
|
||||
x_fire = x_sensor + initial_v_mps * fire_delay
|
||||
q0 = phys.capacitance_f * stage.charge_voltage_v
|
||||
return {
|
||||
"config": config, "initial_x_m": initial_x_m, "initial_v_mps": initial_v_mps,
|
||||
"phys": phys, "q0": q0, "x_fire": x_fire, "v_fire": initial_v_mps,
|
||||
"energy_in": 0.5 * phys.capacitance_f * stage.charge_voltage_v**2,
|
||||
"mass": proj.mass_kg, "stage": stage,
|
||||
}, None
|
||||
|
||||
|
||||
def run_gpu_sweep(
|
||||
db_path: Path,
|
||||
n_runs: int,
|
||||
bounds: SearchBounds = SearchBounds(),
|
||||
data_dir: Path | None = None,
|
||||
seed: int | None = None,
|
||||
prefer_gpu: bool = True,
|
||||
batch_size: int = 20000,
|
||||
log_path: Path | None = None,
|
||||
) -> dict:
|
||||
xp, backend = get_backend(prefer_gpu=prefer_gpu)
|
||||
db = ComponentDatabase.load(data_dir) if data_dir else ComponentDatabase.load()
|
||||
# форсируем одну ступень для GPU-пути
|
||||
bounds = replace(bounds, min_stages=1, max_stages=1)
|
||||
rng = random.Random(seed if seed is not None else random.randrange(2**31))
|
||||
logger = ProgressLogger(log_path or default_log_path(db_path), mode=f"gpu-sweep({backend})", total=n_runs)
|
||||
conn = open_connection(db_path)
|
||||
|
||||
n_feasible = 0
|
||||
done = 0
|
||||
try:
|
||||
while done < n_runs:
|
||||
n = min(batch_size, n_runs - done)
|
||||
genomes = [sample_genome(db, bounds, rng) for _ in range(n)]
|
||||
prepared, records = [], []
|
||||
for g in genomes:
|
||||
p, infeasible = _prepare(g, db, bounds)
|
||||
if p is None:
|
||||
_, reason, config, ix, iv = (None, *infeasible)
|
||||
records.append(_record(g, db, config, None, reason, ix, iv, backend))
|
||||
else:
|
||||
prepared.append((g, p))
|
||||
|
||||
if prepared:
|
||||
models = [p["phys"].inductance_model for _, p in prepared]
|
||||
cps = [p["phys"].circuit_params for _, p in prepared]
|
||||
params = params_from_models(xp, models, cps)
|
||||
out = integrate_batch_discharge(
|
||||
xp,
|
||||
xp.asarray([p["q0"] for _, p in prepared]),
|
||||
xp.asarray([p["x_fire"] for _, p in prepared]),
|
||||
xp.asarray([p["v_fire"] for _, p in prepared]),
|
||||
params, dt=2e-6, max_steps=15000,
|
||||
)
|
||||
exit_v = to_cpu(xp, out["exit_v"]); peak_i = to_cpu(xp, out["peak_current"])
|
||||
e_diss = to_cpu(xp, out["energy_dissipated_j"]); feas = to_cpu(xp, out["feasible"])
|
||||
for k, (g, p) in enumerate(prepared):
|
||||
rec, ok = _finish_record(g, db, p, float(exit_v[k]), float(peak_i[k]),
|
||||
float(e_diss[k]), bool(feas[k]), backend)
|
||||
records.append(rec)
|
||||
if ok:
|
||||
n_feasible += 1
|
||||
|
||||
insert_runs(conn, records)
|
||||
for r in records:
|
||||
logger.update(r.feasible, r.efficiency)
|
||||
done += n
|
||||
finally:
|
||||
logger.finish()
|
||||
conn.close()
|
||||
return {"n_runs": done, "n_feasible": n_feasible, "backend": backend}
|
||||
|
||||
|
||||
def _finish_record(genome, db, p, exit_v, peak_i, e_diss, commutated, backend):
|
||||
stage = p["stage"]
|
||||
energy_in = p["energy_in"]; mass = p["mass"]; v_fire = p["v_fire"]
|
||||
# surge-предел ключа (как в CPU-пути) + должна быть коммутация
|
||||
surge = stage.switch.max_current_a * SWITCH_SURGE_FACTOR.get(stage.switch.kind, 4.0)
|
||||
if not commutated:
|
||||
return _record(genome, db, p["config"], None, "разряд не скоммутировался (батч)", p["initial_x_m"], p["initial_v_mps"], backend), False
|
||||
if peak_i > surge:
|
||||
return _record(genome, db, p["config"], None,
|
||||
f"пиковый ток {peak_i:.0f}А > импульсного предела ключа ({surge:.0f}А)",
|
||||
p["initial_x_m"], p["initial_v_mps"], backend), False
|
||||
kinetic_delta = 0.5 * mass * (exit_v**2 - v_fire**2)
|
||||
efficiency = kinetic_delta / energy_in if energy_in > 0 else None
|
||||
result = _synth_coilgun_result(p, exit_v, peak_i, e_diss, efficiency, kinetic_delta, energy_in)
|
||||
return _record(genome, db, p["config"], result, None, p["initial_x_m"], p["initial_v_mps"], backend, efficiency, exit_v), True
|
||||
|
||||
|
||||
def _synth_coilgun_result(p, exit_v, peak_i, e_diss, efficiency, kinetic_delta, energy_in):
|
||||
r = StageResult(
|
||||
feasible=True, exit_v_mps=exit_v, energy_in_j=energy_in,
|
||||
energy_dissipated_j=e_diss, kinetic_energy_delta_j=kinetic_delta,
|
||||
)
|
||||
outcome = StageOutcome(stage_index=0, result=r, global_coil_center_m=0.0,
|
||||
time_offset_s=0.0, entry_x_m=p["initial_x_m"], entry_v_mps=p["v_fire"])
|
||||
return CoilgunResult(
|
||||
feasible=True, stage_outcomes=[outcome], exit_v_mps=exit_v,
|
||||
exit_kinetic_energy_j=0.5 * p["mass"] * exit_v**2,
|
||||
total_energy_in_j=energy_in, total_energy_dissipated_j=e_diss,
|
||||
total_kinetic_energy_delta_j=kinetic_delta, efficiency=efficiency,
|
||||
)
|
||||
|
||||
|
||||
def _record(genome, db, config, result, reason, initial_x_m, initial_v_mps, backend, efficiency=None, exit_v=None):
|
||||
detail = build_detail(config, result, db, initial_x_m, initial_v_mps, genome.tube_inner_d_m, genome.tube_wall_m) if config else {}
|
||||
return RunRecord(
|
||||
run_id=str(uuid.uuid4()),
|
||||
timestamp=datetime.now(timezone.utc).isoformat(),
|
||||
search_mode=f"gpu-sweep-{backend}",
|
||||
genome_json=json.dumps(genome_to_dict(genome)),
|
||||
decoded_summary_json=json.dumps(detail, ensure_ascii=False),
|
||||
feasible=result is not None,
|
||||
model_version=MODEL_VERSION,
|
||||
infeasible_reason=reason,
|
||||
efficiency=efficiency,
|
||||
exit_velocity_mps=exit_v,
|
||||
cost_rub=None,
|
||||
energy_breakdown_json=None,
|
||||
)
|
||||
@@ -39,7 +39,7 @@ def _tournament_select(
|
||||
|
||||
|
||||
def _continuous_vector(genome: Genome) -> list[float]:
|
||||
vec = [genome.tube_od_m]
|
||||
vec = [genome.tube_inner_d_m, genome.tube_wall_m]
|
||||
for stage in genome.stages:
|
||||
vec.append(stage.sensor_to_coil_distance_m)
|
||||
vec.append(stage.charge_voltage_fraction)
|
||||
@@ -52,7 +52,9 @@ def _continuous_vector(genome: Genome) -> list[float]:
|
||||
def _apply_continuous_vector(template: Genome, vec: list[float]) -> Genome:
|
||||
genome = copy.deepcopy(template)
|
||||
idx = 0
|
||||
genome.tube_od_m = vec[idx]
|
||||
genome.tube_inner_d_m = vec[idx]
|
||||
idx += 1
|
||||
genome.tube_wall_m = vec[idx]
|
||||
idx += 1
|
||||
for stage in genome.stages:
|
||||
stage.sensor_to_coil_distance_m = vec[idx]
|
||||
|
||||
@@ -88,7 +88,7 @@ def decoded_summary(genome: Genome, db: ComponentDatabase) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def build_detail(config, coilgun_result: CoilgunResult, db: ComponentDatabase, initial_x_m: float, initial_v_mps: float) -> dict:
|
||||
def build_detail(config, coilgun_result: CoilgunResult, db: ComponentDatabase, initial_x_m: float, initial_v_mps: float, tube_inner_d_m: float | None = None, tube_wall_m: float | None = None) -> dict:
|
||||
"""Максимально подробная запись: параметры + вычисленная физика + результат каждой ступени.
|
||||
|
||||
Именно это пишется в SQLite (decoded_summary_json), чтобы по базе можно
|
||||
@@ -96,7 +96,7 @@ def build_detail(config, coilgun_result: CoilgunResult, db: ComponentDatabase, i
|
||||
вдоль трубы, номиналы всех деталей, геометрию намотки, посчитанные
|
||||
индуктивность/сопротивление/пиковый ток и что произошло на каждой ступени.
|
||||
"""
|
||||
outcomes_by_index = {o.stage_index: o for o in coilgun_result.stage_outcomes}
|
||||
outcomes_by_index = {o.stage_index: o for o in (coilgun_result.stage_outcomes if coilgun_result else [])}
|
||||
projectile = config.projectile
|
||||
|
||||
# абсолютные позиции центров катушек вдоль трубы (сквозная координата)
|
||||
@@ -131,27 +131,37 @@ def build_detail(config, coilgun_result: CoilgunResult, db: ComponentDatabase, i
|
||||
"wire": stage.wire.part_id,
|
||||
"wire_material": stage.wire.material,
|
||||
"wire_gauge_mm": stage.wire.gauge_mm,
|
||||
"wire_insulation_od_mm": stage.wire.insulation_od_mm,
|
||||
"wire_url": stage.wire.url,
|
||||
"switch": stage.switch.part_number,
|
||||
"switch_kind": stage.switch.kind,
|
||||
"switch_max_current_a": stage.switch.max_current_a,
|
||||
"switch_max_voltage_v": stage.switch.max_voltage_v,
|
||||
"switch_url": stage.switch.url,
|
||||
"sensor": stage.sensor.part_number,
|
||||
"sensor_kind": stage.sensor.kind,
|
||||
"sensor_url": stage.sensor.url,
|
||||
},
|
||||
"capacitor_bank": {
|
||||
"base_part": cap_base,
|
||||
"n_series": n_series,
|
||||
"n_parallel": n_parallel,
|
||||
"count": n_series * n_parallel,
|
||||
"single_capacitance_uf": getattr(cap, "capacitance_uf", 0) * n_series / max(n_parallel, 1),
|
||||
"single_voltage_rating_v": getattr(cap, "voltage_v", 0) / max(n_series, 1),
|
||||
"total_capacitance_uf": cap.capacitance_uf,
|
||||
"total_voltage_rating_v": cap.voltage_v,
|
||||
"total_esr_ohm": cap.esr_ohm,
|
||||
"max_current_a": cap.max_current_a,
|
||||
"url": getattr(cap, "url", None),
|
||||
},
|
||||
"winding": {
|
||||
"turns_per_layer": stage.turns_per_layer,
|
||||
"layers": stage.layers,
|
||||
"total_turns": geometry.total_turns,
|
||||
"coil_length_m": geometry.coil_length_m,
|
||||
"coil_inner_diameter_m": stage.tube_od_m, # = внешний диаметр трубы
|
||||
"coil_outer_diameter_m": stage.tube_od_m + 2 * geometry.radial_depth_m,
|
||||
"mean_radius_m": geometry.mean_radius_m,
|
||||
"radial_depth_m": geometry.radial_depth_m,
|
||||
"total_wire_length_m": geometry.total_wire_length_m,
|
||||
@@ -197,16 +207,26 @@ def build_detail(config, coilgun_result: CoilgunResult, db: ComponentDatabase, i
|
||||
entry["outcome"] = {"reached": False}
|
||||
stages_detail.append(entry)
|
||||
|
||||
tube_od_m = config.stages[0].tube_od_m if config.stages else None
|
||||
return {
|
||||
"tube_od_m": config.stages[0].tube_od_m if config.stages else None,
|
||||
"tube": {
|
||||
"inner_diameter_m": tube_inner_d_m, # бор — снаряд летит внутри
|
||||
"wall_thickness_m": tube_wall_m,
|
||||
"outer_diameter_m": tube_od_m, # = внутренний диаметр катушки
|
||||
},
|
||||
"tube_od_m": tube_od_m, # для обратной совместимости
|
||||
"tube_length_m": (coil_centers_m[-1] + 0.05) if coil_centers_m else None,
|
||||
"projectile": {
|
||||
"material": projectile.material.name,
|
||||
"diameter_m": projectile.diameter_m,
|
||||
"length_m": projectile.length_m,
|
||||
"mass_kg": projectile.mass_kg,
|
||||
"mass_g": projectile.mass_kg * 1000,
|
||||
"mu_r": projectile.material.mu_r,
|
||||
"b_sat_tesla": projectile.material.b_sat_tesla,
|
||||
"resistivity_ohm_m": projectile.material.resistivity_ohm_m,
|
||||
"fits_in_bore": (tube_inner_d_m is None) or (projectile.diameter_m < tube_inner_d_m),
|
||||
"url": projectile.material.url,
|
||||
},
|
||||
"launch": {"initial_x_m": initial_x_m, "initial_v_mps": initial_v_mps},
|
||||
"n_stages": len(config.stages),
|
||||
@@ -220,7 +240,7 @@ def evaluate(genome: Genome, db: ComponentDatabase, bounds: SearchBounds) -> Eva
|
||||
config, initial_x_m, initial_v_mps = decode(genome, db, bounds)
|
||||
cost_rub = compute_cost_rub(config, db)
|
||||
result = run_coilgun(config)
|
||||
detail = build_detail(config, result, db, initial_x_m, initial_v_mps)
|
||||
detail = build_detail(config, result, db, initial_x_m, initial_v_mps, genome.tube_inner_d_m, genome.tube_wall_m)
|
||||
|
||||
if not result.feasible:
|
||||
n_stages = len(config.stages)
|
||||
|
||||
@@ -16,7 +16,8 @@ from gausse.components.schema import CapacitorBank
|
||||
from gausse.sim.coilgun import CoilgunConfig
|
||||
from gausse.sim.stage import ProjectileConfig, StageConfig
|
||||
|
||||
TUBE_WALL_CLEARANCE_M = 0.001
|
||||
# зазор между снарядом и внутренней стенкой трубы (бором), чтобы снаряд летел свободно
|
||||
PROJECTILE_BORE_CLEARANCE_M = 0.0005
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -31,12 +32,18 @@ class SearchBounds:
|
||||
sensor_to_coil_distance_m_max: float = 0.05
|
||||
inter_stage_gap_m_min: float = 0.01
|
||||
inter_stage_gap_m_max: float = 0.10
|
||||
tube_od_m_min: float = 0.009
|
||||
tube_od_m_max: float = 0.02
|
||||
# ТРУБА: внутренний диаметр (бор — снаряд летит внутри) и толщина стенки.
|
||||
# Внешний диаметр = внутренний + 2×стенка = внутренний диаметр КАТУШКИ.
|
||||
tube_inner_d_m_min: float = 0.005
|
||||
tube_inner_d_m_max: float = 0.030
|
||||
tube_wall_m_min: float = 0.001
|
||||
tube_wall_m_max: float = 0.003
|
||||
# СНАРЯД: диаметр обязан быть < внутр. диаметра трубы; масса до 200 г.
|
||||
projectile_diameter_m_min: float = 0.004
|
||||
projectile_diameter_m_max: float = 0.008
|
||||
projectile_diameter_m_max: float = 0.028
|
||||
projectile_length_m_min: float = 0.01
|
||||
projectile_length_m_max: float = 0.03
|
||||
projectile_length_m_max: float = 0.15
|
||||
projectile_mass_max_kg: float = 0.2
|
||||
charge_voltage_fraction_min: float = 0.5
|
||||
charge_voltage_fraction_max: float = 1.0
|
||||
initial_launch_velocity_mps: float = 3.0
|
||||
@@ -48,6 +55,12 @@ class SearchBounds:
|
||||
cap_parallel_max: int = 8
|
||||
|
||||
|
||||
def _max_length_for_mass(diameter_m: float, density_kg_m3: float, max_mass_kg: float) -> float:
|
||||
"""Максимальная длина снаряда, при которой масса не превышает max_mass_kg."""
|
||||
area = math.pi * (diameter_m / 2) ** 2
|
||||
return max_mass_kg / (density_kg_m3 * area)
|
||||
|
||||
|
||||
@dataclass
|
||||
class StageGene:
|
||||
wire_idx: int
|
||||
@@ -71,11 +84,17 @@ class ProjectileGene:
|
||||
|
||||
@dataclass
|
||||
class Genome:
|
||||
tube_od_m: float
|
||||
tube_inner_d_m: float # бор трубы (снаряд летит внутри)
|
||||
tube_wall_m: float # толщина стенки трубы
|
||||
stages: list[StageGene]
|
||||
inter_stage_gaps_m: list[float]
|
||||
projectile: ProjectileGene
|
||||
|
||||
@property
|
||||
def tube_od_m(self) -> float:
|
||||
"""Внешний диаметр трубы = внутренний диаметр катушки."""
|
||||
return self.tube_inner_d_m + 2 * self.tube_wall_m
|
||||
|
||||
|
||||
def _clip(value: float, lo: float, hi: float) -> float:
|
||||
return max(lo, min(hi, value))
|
||||
@@ -101,36 +120,47 @@ def sample_stage_gene(db: ComponentDatabase, bounds: SearchBounds, rng: random.R
|
||||
|
||||
|
||||
def sample_genome(db: ComponentDatabase, bounds: SearchBounds, rng: random.Random) -> Genome:
|
||||
tube_od_m = rng.uniform(bounds.tube_od_m_min, bounds.tube_od_m_max)
|
||||
max_diameter = min(bounds.projectile_diameter_m_max, tube_od_m - TUBE_WALL_CLEARANCE_M)
|
||||
tube_inner_d_m = rng.uniform(bounds.tube_inner_d_m_min, bounds.tube_inner_d_m_max)
|
||||
tube_wall_m = rng.uniform(bounds.tube_wall_m_min, bounds.tube_wall_m_max)
|
||||
|
||||
# диаметр снаряда < внутр. диаметра трубы минус зазор
|
||||
max_diameter = min(bounds.projectile_diameter_m_max, tube_inner_d_m - PROJECTILE_BORE_CLEARANCE_M)
|
||||
diameter_m = rng.uniform(bounds.projectile_diameter_m_min, max(max_diameter, bounds.projectile_diameter_m_min))
|
||||
|
||||
material_idx = rng.randrange(len(db.projectile_materials))
|
||||
density = db.projectile_materials[material_idx].density_kg_m3
|
||||
# длина ограничена и границами, и максимальной массой 200 г
|
||||
len_cap = min(bounds.projectile_length_m_max, _max_length_for_mass(diameter_m, density, bounds.projectile_mass_max_kg))
|
||||
length_m = rng.uniform(bounds.projectile_length_m_min, max(len_cap, bounds.projectile_length_m_min))
|
||||
|
||||
n_stages = rng.randint(bounds.min_stages, bounds.max_stages)
|
||||
stages = [sample_stage_gene(db, bounds, rng) for _ in range(n_stages)]
|
||||
gaps = [
|
||||
rng.uniform(bounds.inter_stage_gap_m_min, bounds.inter_stage_gap_m_max)
|
||||
for _ in range(n_stages - 1)
|
||||
]
|
||||
projectile = ProjectileGene(
|
||||
material_idx=rng.randrange(len(db.projectile_materials)),
|
||||
diameter_m=diameter_m,
|
||||
length_m=rng.uniform(bounds.projectile_length_m_min, bounds.projectile_length_m_max),
|
||||
projectile = ProjectileGene(material_idx=material_idx, diameter_m=diameter_m, length_m=length_m)
|
||||
return Genome(
|
||||
tube_inner_d_m=tube_inner_d_m, tube_wall_m=tube_wall_m,
|
||||
stages=stages, inter_stage_gaps_m=gaps, projectile=projectile,
|
||||
)
|
||||
return Genome(tube_od_m=tube_od_m, stages=stages, inter_stage_gaps_m=gaps, projectile=projectile)
|
||||
|
||||
|
||||
def repair(genome: Genome, db: ComponentDatabase, bounds: SearchBounds) -> Genome:
|
||||
"""Приводит геном в границы после мутации/скрещивания (клэмп, не отбраковка)."""
|
||||
genome.tube_od_m = _clip(genome.tube_od_m, bounds.tube_od_m_min, bounds.tube_od_m_max)
|
||||
max_diameter = max(genome.tube_od_m - TUBE_WALL_CLEARANCE_M, bounds.projectile_diameter_m_min)
|
||||
genome.projectile.diameter_m = _clip(
|
||||
genome.projectile.diameter_m, bounds.projectile_diameter_m_min, max_diameter
|
||||
)
|
||||
genome.projectile.length_m = _clip(
|
||||
genome.projectile.length_m, bounds.projectile_length_m_min, bounds.projectile_length_m_max
|
||||
)
|
||||
genome.tube_inner_d_m = _clip(genome.tube_inner_d_m, bounds.tube_inner_d_m_min, bounds.tube_inner_d_m_max)
|
||||
genome.tube_wall_m = _clip(genome.tube_wall_m, bounds.tube_wall_m_min, bounds.tube_wall_m_max)
|
||||
genome.projectile.material_idx = genome.projectile.material_idx % len(db.projectile_materials)
|
||||
|
||||
# снаряд обязан влезать в бор трубы
|
||||
max_diameter = max(genome.tube_inner_d_m - PROJECTILE_BORE_CLEARANCE_M, bounds.projectile_diameter_m_min)
|
||||
max_diameter = min(max_diameter, bounds.projectile_diameter_m_max)
|
||||
genome.projectile.diameter_m = _clip(genome.projectile.diameter_m, bounds.projectile_diameter_m_min, max_diameter)
|
||||
# длина в границах И под массой ≤ 200 г
|
||||
density = db.projectile_materials[genome.projectile.material_idx].density_kg_m3
|
||||
len_cap = min(bounds.projectile_length_m_max, _max_length_for_mass(genome.projectile.diameter_m, density, bounds.projectile_mass_max_kg))
|
||||
genome.projectile.length_m = _clip(genome.projectile.length_m, bounds.projectile_length_m_min, max(len_cap, bounds.projectile_length_m_min))
|
||||
|
||||
for stage in genome.stages:
|
||||
stage.wire_idx %= len(db.wires)
|
||||
stage.capacitor_idx %= len(db.capacitors)
|
||||
@@ -187,7 +217,7 @@ def decode(genome: Genome, db: ComponentDatabase, bounds: SearchBounds) -> tuple
|
||||
capacitor=capacitor,
|
||||
switch=switch,
|
||||
sensor=sensor,
|
||||
tube_od_m=genome.tube_od_m,
|
||||
tube_od_m=genome.tube_od_m, # внешний диаметр трубы = внутр. диаметр катушки
|
||||
turns_per_layer=gene.turns_per_layer,
|
||||
layers=gene.layers,
|
||||
sensor_to_coil_distance_m=gene.sensor_to_coil_distance_m,
|
||||
@@ -209,7 +239,9 @@ def mutate(genome: Genome, db: ComponentDatabase, bounds: SearchBounds, rng: ran
|
||||
child = copy.deepcopy(genome)
|
||||
|
||||
if rng.random() < rate:
|
||||
child.tube_od_m = rng.uniform(bounds.tube_od_m_min, bounds.tube_od_m_max)
|
||||
child.tube_inner_d_m = rng.uniform(bounds.tube_inner_d_m_min, bounds.tube_inner_d_m_max)
|
||||
if rng.random() < rate:
|
||||
child.tube_wall_m = rng.uniform(bounds.tube_wall_m_min, bounds.tube_wall_m_max)
|
||||
|
||||
for stage in child.stages:
|
||||
if rng.random() < rate:
|
||||
@@ -295,7 +327,8 @@ def crossover(parent_a: Genome, parent_b: Genome, rng: random.Random) -> Genome:
|
||||
tube_source = parent_a if rng.random() < 0.5 else parent_b
|
||||
|
||||
return Genome(
|
||||
tube_od_m=tube_source.tube_od_m,
|
||||
tube_inner_d_m=tube_source.tube_inner_d_m,
|
||||
tube_wall_m=tube_source.tube_wall_m,
|
||||
stages=stages,
|
||||
inter_stage_gaps_m=gaps,
|
||||
projectile=copy.deepcopy(projectile_source.projectile),
|
||||
@@ -304,7 +337,8 @@ def crossover(parent_a: Genome, parent_b: Genome, rng: random.Random) -> Genome:
|
||||
|
||||
def genome_to_dict(genome: Genome) -> dict:
|
||||
return {
|
||||
"tube_od_m": genome.tube_od_m,
|
||||
"tube_inner_d_m": genome.tube_inner_d_m,
|
||||
"tube_wall_m": genome.tube_wall_m,
|
||||
"stages": [vars(s) for s in genome.stages],
|
||||
"inter_stage_gaps_m": genome.inter_stage_gaps_m,
|
||||
"projectile": vars(genome.projectile),
|
||||
@@ -312,8 +346,16 @@ def genome_to_dict(genome: Genome) -> dict:
|
||||
|
||||
|
||||
def genome_from_dict(data: dict) -> Genome:
|
||||
# обратная совместимость: старые геномы в БД несли tube_od_m без стенки
|
||||
if "tube_inner_d_m" in data:
|
||||
tube_inner_d_m = data["tube_inner_d_m"]
|
||||
tube_wall_m = data["tube_wall_m"]
|
||||
else:
|
||||
tube_wall_m = 0.0015
|
||||
tube_inner_d_m = max(data.get("tube_od_m", 0.012) - 2 * tube_wall_m, 0.005)
|
||||
return Genome(
|
||||
tube_od_m=data["tube_od_m"],
|
||||
tube_inner_d_m=tube_inner_d_m,
|
||||
tube_wall_m=tube_wall_m,
|
||||
stages=[StageGene(**s) for s in data["stages"]],
|
||||
inter_stage_gaps_m=list(data["inter_stage_gaps_m"]),
|
||||
projectile=ProjectileGene(**data["projectile"]),
|
||||
|
||||
@@ -26,6 +26,14 @@ class StageCircuitParams:
|
||||
capacitance_f: float
|
||||
r_total_ohm: float
|
||||
mass_kg: float
|
||||
# отражённое сопротивление вихревых токов снаряда; действует × overlap(x)
|
||||
# (только пока снаряд в катушке). См. physics/losses.py.
|
||||
r_eddy_coeff_ohm: float = 0.0
|
||||
|
||||
|
||||
def effective_resistance(inductance_model: CoilInductanceModel, params: StageCircuitParams, x) -> float:
|
||||
"""R контура = провод+ESR+ключ (r_total) плюс вихревые потери снаряда, когда он в катушке."""
|
||||
return params.r_total_ohm + params.r_eddy_coeff_ohm * inductance_model.overlap_fraction(x)
|
||||
|
||||
|
||||
def derivatives(
|
||||
@@ -37,10 +45,11 @@ def derivatives(
|
||||
q, current, x, v = state
|
||||
dlambda_di = inductance_model.dlambda_di(x, current)
|
||||
dlambda_dx = inductance_model.dlambda_dx(x, current)
|
||||
r_eff = effective_resistance(inductance_model, params, x)
|
||||
|
||||
v_c = q / params.capacitance_f
|
||||
d_q = -current
|
||||
d_i = (v_c - current * params.r_total_ohm - dlambda_dx * v) / dlambda_di
|
||||
d_i = (v_c - current * r_eff - dlambda_dx * v) / dlambda_di
|
||||
force = inductance_model.force_newtons(x, current)
|
||||
d_v = force / params.mass_kg
|
||||
return [d_q, d_i, v, d_v]
|
||||
@@ -52,3 +61,24 @@ def zero_current_crossing_event(t: float, state: np.ndarray, *_args) -> float:
|
||||
|
||||
zero_current_crossing_event.terminal = True
|
||||
zero_current_crossing_event.direction = -1.0
|
||||
|
||||
|
||||
def current_local_min_event(t: float, state: np.ndarray, inductance_model, params) -> float:
|
||||
"""Первый локальный МИНИМУМ тока после пика — там снаряд начинает
|
||||
«подкачивать» ток обратно (генераторный режим за центром катушки).
|
||||
|
||||
Тиристор проводит ОДИН импульс: как только ток, спадая, перестаёт падать
|
||||
и пошёл бы вверх, ключ запирается (в реальности так и есть — за один
|
||||
выстрел катушка стреляет один раз, конденсатор не перезаряжается за мкс).
|
||||
Событие = dI/dt, ловим переход снизу вверх (direction=+1).
|
||||
"""
|
||||
q, current, x, v = state
|
||||
dlambda_di = inductance_model.dlambda_di(x, current)
|
||||
dlambda_dx = inductance_model.dlambda_dx(x, current)
|
||||
r_eff = effective_resistance(inductance_model, params, x)
|
||||
v_c = q / params.capacitance_f
|
||||
return (v_c - current * r_eff - dlambda_dx * v) / dlambda_di
|
||||
|
||||
|
||||
current_local_min_event.terminal = True
|
||||
current_local_min_event.direction = 1.0
|
||||
|
||||
@@ -175,6 +175,16 @@ class CoilInductanceModel:
|
||||
"""Сила из coenergy: F = ∂/∂x ∫₀ᴵ λ dI' = L_iron·overlap'(x)·G(I)."""
|
||||
return self.l_iron_coeff * self.d_overlap_dx(x) * self._g_integral(i)
|
||||
|
||||
def magnetic_energy(self, x: np.ndarray, i: np.ndarray) -> np.ndarray:
|
||||
"""Энергия магнитного поля W = ∫₀ᴵ i·(∂λ/∂i) di (с учётом насыщения).
|
||||
|
||||
= ½·L_air·I² + L_iron·overlap(x)·(I·g(I) − G(I)). Нужна для точного
|
||||
учёта остаточной энергии катушки при обрыве разряда (одиночный импульс).
|
||||
"""
|
||||
return 0.5 * self.l_air_h * i**2 + self.l_iron_coeff * self.overlap_fraction(x) * (
|
||||
i * self._g(i) - self._g_integral(i)
|
||||
)
|
||||
|
||||
def effective_field_current_a(self, i: float) -> float:
|
||||
"""'Насыщенный' эффективный ток для оценки поля (поле ~ g(I), кап B_sat)."""
|
||||
return float(self._g(np.asarray(float(i))))
|
||||
|
||||
55
src/gausse/physics/losses.py
Normal file
55
src/gausse/physics/losses.py
Normal file
@@ -0,0 +1,55 @@
|
||||
"""Потери в железе снаряда: вихревые токи + гистерезис (первого порядка).
|
||||
|
||||
Сплошной стальной снаряд в импульсном поле теряет энергию на вихревые токи
|
||||
(греется). Модель: снаряд — короткозамкнутый виток (вторичка трансформатора),
|
||||
резистивно-доминированный на частоте импульса. Отражённое в катушку
|
||||
сопротивление R_eddy = (ω·M)²/R_e добавляется в контур, ПОКА снаряд в
|
||||
катушке (× overlap(x)) — энергия честно уходит в нагрев снаряда, а не в
|
||||
кинетику. Это и есть тот канал потерь, из-за отсутствия которого КПД был
|
||||
нереально высоким.
|
||||
|
||||
ВАЖНО (честно): это оценка первого порядка. Точные вихревые потери требуют
|
||||
решения уравнения диффузии поля в снаряде (скин-эффект, частотная
|
||||
зависимость). Здесь — сосредоточенный виток + характерная частота импульса
|
||||
ω=1/√(L·C). Геометрия вихревого контура (R_e) и коэффициент связи оценены
|
||||
приближённо; величина может отличаться в разы. Коэффициенты вынесены, чтобы
|
||||
их можно было уточнить по реальным замерам.
|
||||
"""
|
||||
|
||||
import math
|
||||
|
||||
from gausse.physics.constants import MU_0
|
||||
|
||||
# доля потока катушки, реально сцепленная с вихревым контуром снаряда (k<1).
|
||||
# Снаряд у́же катушки и захватывает не весь поток — грубая оценка.
|
||||
EDDY_COUPLING_FACTOR = 0.5
|
||||
# гистерезис: энергия на площадь петли B-H за перемагничивание, Дж/м³/цикл.
|
||||
# Для конструкционной стали ~сотни Дж/м³; берём ~300 (справочная оценка).
|
||||
HYSTERESIS_LOSS_J_PER_M3 = 300.0
|
||||
|
||||
|
||||
def eddy_reflected_resistance_ohm(
|
||||
slug_radius_m: float,
|
||||
slug_length_m: float,
|
||||
slug_resistivity_ohm_m: float,
|
||||
mu_eff: float,
|
||||
total_turns: int,
|
||||
coil_length_m: float,
|
||||
char_omega_rad_s: float,
|
||||
) -> float:
|
||||
"""Отражённое в катушку сопротивление вихревого контура снаряда (Ом)."""
|
||||
a_slug = math.pi * slug_radius_m**2
|
||||
turns_per_m = total_turns / coil_length_m
|
||||
# взаимная индуктивность катушка↔снаряд (снаряд как 1 виток)
|
||||
mutual = EDDY_COUPLING_FACTOR * MU_0 * mu_eff * turns_per_m * a_slug
|
||||
# сопротивление сосредоточенного вихревого контура (кольцевой путь в снаряде)
|
||||
r_eddy_loop = 2 * math.pi * slug_resistivity_ohm_m / slug_length_m
|
||||
if r_eddy_loop <= 0:
|
||||
return 0.0
|
||||
return (char_omega_rad_s * mutual) ** 2 / r_eddy_loop
|
||||
|
||||
|
||||
def hysteresis_energy_j(slug_radius_m: float, slug_length_m: float, n_cycles: float = 1.0) -> float:
|
||||
"""Энергия гистерезиса за выстрел (Дж) — вторичный канал, обычно << вихревых."""
|
||||
volume = math.pi * slug_radius_m**2 * slug_length_m
|
||||
return HYSTERESIS_LOSS_J_PER_M3 * volume * n_cycles
|
||||
@@ -21,44 +21,57 @@ from gausse.sim.coilgun import CoilgunConfig, CoilgunResult
|
||||
|
||||
|
||||
def _build_global_timeline(result: CoilgunResult, config: CoilgunConfig, fps: int):
|
||||
"""Возвращает (times, positions, per_stage_current_at_time) с равномерным шагом."""
|
||||
segments_t = []
|
||||
segments_x = []
|
||||
segments_i = [] # ток КАЖДОЙ катушки в момент времени (список массивов той же длины, что t)
|
||||
"""Непрерывная траектория снаряда: коаст (по инерции) + разряды по ступеням.
|
||||
|
||||
Строится ПОСЛЕДОВАТЕЛЬНО (без сортировки по времени, которая рвала
|
||||
траекторию): между разрядами снаряд летит по инерции от конца прошлого
|
||||
разряда до начала следующего; во время разряда берём точно посчитанные
|
||||
discharge_x/v/i. Глобальная координата = центр катушки ступени + локальный
|
||||
discharge_x. Ток каждой катушки задан только в окне её собственного разряда.
|
||||
"""
|
||||
n_stages = len(config.stages)
|
||||
seg_t, seg_x = [], []
|
||||
seg_i = [[] for _ in range(n_stages)]
|
||||
|
||||
t_cursor = 0.0
|
||||
prev_global_x = None
|
||||
prev_v = None
|
||||
|
||||
for outcome in result.stage_outcomes:
|
||||
r = outcome.result
|
||||
# Баллистический подлёт к датчику: постоянная скорость, восстанавливается аналитически
|
||||
# (в модели v1 нет трения на этом участке — см. sim/stage.py::_ballistic_derivatives).
|
||||
t_sensor = r.t_sensor_s if r.t_sensor_s is not None else 0.0
|
||||
flight_t = np.linspace(0.0, t_sensor, max(int(t_sensor * fps) + 2, 2))
|
||||
flight_x = outcome.entry_x_m + outcome.entry_v_mps * flight_t
|
||||
flight_global_t = outcome.time_offset_s + flight_t
|
||||
flight_global_x = outcome.global_coil_center_m + flight_x
|
||||
flight_currents = [np.zeros_like(flight_t) for _ in range(n_stages)]
|
||||
flight_currents[outcome.stage_index] = np.zeros_like(flight_t)
|
||||
segments_t.append(flight_global_t)
|
||||
segments_x.append(flight_global_x)
|
||||
segments_i.append(flight_currents)
|
||||
if r.discharge_t is None or r.discharge_x is None:
|
||||
continue
|
||||
disch_global_x = outcome.global_coil_center_m + r.discharge_x
|
||||
start_global_x = float(disch_global_x[0])
|
||||
|
||||
if r.discharge_t is not None:
|
||||
discharge_global_t = outcome.time_offset_s + (r.t_fire_s or 0.0) + r.discharge_t
|
||||
discharge_global_x = outcome.global_coil_center_m + r.discharge_x
|
||||
discharge_currents = [np.zeros_like(r.discharge_t) for _ in range(n_stages)]
|
||||
discharge_currents[outcome.stage_index] = r.discharge_i
|
||||
segments_t.append(discharge_global_t)
|
||||
segments_x.append(discharge_global_x)
|
||||
segments_i.append(discharge_currents)
|
||||
# коаст по инерции от конца прошлого разряда до начала этого
|
||||
if prev_global_x is not None and prev_v and prev_v > 1e-6:
|
||||
coast_dist = start_global_x - prev_global_x
|
||||
coast_dur = max(coast_dist / prev_v, 0.0)
|
||||
n_coast = max(int(coast_dur * fps) + 2, 2)
|
||||
ct = np.linspace(0.0, coast_dur, n_coast)
|
||||
seg_t.append(t_cursor + ct)
|
||||
seg_x.append(np.linspace(prev_global_x, start_global_x, n_coast))
|
||||
for s in range(n_stages):
|
||||
seg_i[s].append(np.zeros(n_coast))
|
||||
t_cursor += coast_dur
|
||||
|
||||
times = np.concatenate(segments_t)
|
||||
positions = np.concatenate(segments_x)
|
||||
currents = [np.concatenate([seg[stage] for seg in segments_i]) for stage in range(n_stages)]
|
||||
# окно разряда: точная траектория + ток именно этой катушки
|
||||
seg_t.append(t_cursor + r.discharge_t)
|
||||
seg_x.append(disch_global_x)
|
||||
for s in range(n_stages):
|
||||
seg_i[s].append(r.discharge_i if s == outcome.stage_index else np.zeros_like(r.discharge_t))
|
||||
t_cursor += float(r.discharge_t[-1])
|
||||
|
||||
order = np.argsort(times)
|
||||
times = times[order]
|
||||
positions = positions[order]
|
||||
currents = [c[order] for c in currents]
|
||||
prev_global_x = float(disch_global_x[-1])
|
||||
prev_v = float(r.discharge_v[-1]) if r.discharge_v is not None else outcome.entry_v_mps
|
||||
|
||||
if not seg_t:
|
||||
raise ValueError("нет данных разряда для анимации")
|
||||
|
||||
times = np.concatenate(seg_t)
|
||||
positions = np.concatenate(seg_x)
|
||||
currents = [np.concatenate(seg_i[s]) for s in range(n_stages)]
|
||||
return times, positions, currents
|
||||
|
||||
|
||||
@@ -81,20 +94,30 @@ def animate_run(result: CoilgunResult, config: CoilgunConfig, out_path: str, fps
|
||||
ax.set_yticks([])
|
||||
ax.set_title("Пролёт снаряда через ступени")
|
||||
|
||||
tube_line = ax.axhline(0, color="gray", linewidth=2, zorder=1)
|
||||
coil_dots = [
|
||||
ax.scatter([c * 1e3], [0], s=400, c="lightgray", edgecolors="black", zorder=2)
|
||||
for c in coil_centers
|
||||
]
|
||||
slug_dot = ax.scatter([], [], s=150, c="red", zorder=3)
|
||||
time_text = ax.text(0.02, 0.9, "", transform=ax.transAxes)
|
||||
ax.axhline(0, color="#888", linewidth=3, zorder=1) # труба
|
||||
coil_dots = []
|
||||
for k, c in enumerate(coil_centers):
|
||||
dot = ax.scatter([c * 1e3], [0], s=600, c="lightgray", edgecolors="#333", linewidths=1.5, zorder=2)
|
||||
ax.annotate(f"катушка {k}", (c * 1e3, 0), textcoords="offset points", xytext=(0, 22),
|
||||
ha="center", fontsize=8, color="#555")
|
||||
coil_dots.append(dot)
|
||||
slug_dot = ax.scatter([], [], s=180, c="#1f4e8c", edgecolors="white", linewidths=1, zorder=3)
|
||||
time_text = ax.text(0.02, 0.88, "", transform=ax.transAxes, fontsize=10)
|
||||
|
||||
def _coil_color(intensity):
|
||||
# выкл -> светло-серый; максимум тока -> яркое золото (свечение поля)
|
||||
t = min(max(intensity, 0.0), 1.0)
|
||||
off = np.array([0.83, 0.83, 0.83])
|
||||
on = np.array([1.0, 0.78, 0.0])
|
||||
return tuple(off + (on - off) * t)
|
||||
|
||||
def update(frame_i):
|
||||
idx = frame_idx[frame_i]
|
||||
slug_dot.set_offsets([[positions[idx] * 1e3, 0]])
|
||||
for stage_idx, dots in enumerate(coil_dots):
|
||||
intensity = abs(currents[stage_idx][idx]) / max_current_per_stage[stage_idx]
|
||||
dots.set_color(plt.cm.autumn(1.0 - min(intensity, 1.0)))
|
||||
dots.set_color(_coil_color(intensity))
|
||||
dots.set_sizes([600 + 400 * min(intensity, 1.0)]) # чуть раздувается при разряде
|
||||
time_text.set_text(f"t = {times[idx] * 1e3:.3f} мс")
|
||||
return [slug_dot, time_text, *coil_dots]
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ class BomLine:
|
||||
unit_price_rub: float
|
||||
total_price_rub: float
|
||||
note: str = ""
|
||||
url: str | None = None # ссылка на магазин, где купить
|
||||
|
||||
|
||||
def build_bom(config: CoilgunConfig, db: ComponentDatabase) -> list[BomLine]:
|
||||
@@ -30,7 +31,8 @@ def build_bom(config: CoilgunConfig, db: ComponentDatabase) -> list[BomLine]:
|
||||
quantity=f"{wire_length_m:.2f} м",
|
||||
unit_price_rub=stage.wire.price_per_m,
|
||||
total_price_rub=wire_length_m * stage.wire.price_per_m,
|
||||
note=f"{geometry.total_turns} витков ({stage.turns_per_layer}x{stage.layers})",
|
||||
note=f"{geometry.total_turns} витков ({stage.turns_per_layer}x{stage.layers}), ⌀{stage.wire.gauge_mm}мм",
|
||||
url=stage.wire.url,
|
||||
)
|
||||
)
|
||||
cap = stage.capacitor
|
||||
@@ -47,6 +49,7 @@ def build_bom(config: CoilgunConfig, db: ComponentDatabase) -> list[BomLine]:
|
||||
unit_price_rub=unit_price,
|
||||
total_price_rub=cap.price,
|
||||
note=f"батарея {cap.capacitance_uf:.0f}мкФ {cap.voltage_v:.0f}В, заряд {stage.charge_voltage_v:.0f}В, макс.ток {cap.max_current_a:.0f}А",
|
||||
url=getattr(cap, "url", None),
|
||||
)
|
||||
)
|
||||
lines.append(
|
||||
@@ -56,7 +59,8 @@ def build_bom(config: CoilgunConfig, db: ComponentDatabase) -> list[BomLine]:
|
||||
quantity="1 шт",
|
||||
unit_price_rub=stage.switch.price,
|
||||
total_price_rub=stage.switch.price,
|
||||
note=stage.switch.kind,
|
||||
note=f"{stage.switch.kind}, {stage.switch.max_current_a:.0f}А / {stage.switch.max_voltage_v:.0f}В",
|
||||
url=stage.switch.url,
|
||||
)
|
||||
)
|
||||
lines.append(
|
||||
@@ -67,6 +71,7 @@ def build_bom(config: CoilgunConfig, db: ComponentDatabase) -> list[BomLine]:
|
||||
unit_price_rub=stage.sensor.price,
|
||||
total_price_rub=stage.sensor.price,
|
||||
note=stage.sensor.kind,
|
||||
url=stage.sensor.url,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -79,6 +84,7 @@ def build_bom(config: CoilgunConfig, db: ComponentDatabase) -> list[BomLine]:
|
||||
unit_price_rub=config.projectile.material.price_per_kg,
|
||||
total_price_rub=mass_kg * config.projectile.material.price_per_kg,
|
||||
note=f"⌀{config.projectile.diameter_m * 1000:.1f}мм x {config.projectile.length_m * 1000:.1f}мм",
|
||||
url=config.projectile.material.url,
|
||||
)
|
||||
)
|
||||
return lines
|
||||
@@ -89,12 +95,12 @@ def total_cost_rub(lines: list[BomLine]) -> float:
|
||||
|
||||
|
||||
def render_bom_text(lines: list[BomLine]) -> str:
|
||||
rows = ["Ступень\tДеталь\tКол-во\tЦена/ед\tИтого\tПримечание"]
|
||||
rows = ["Ступень\tДеталь\tКол-во\tЦена/ед\tИтого\tПримечание\tКупить"]
|
||||
for line in lines:
|
||||
stage_label = "-" if line.stage_index is None else str(line.stage_index)
|
||||
rows.append(
|
||||
f"{stage_label}\t{line.part}\t{line.quantity}\t{line.unit_price_rub:.2f}₽\t"
|
||||
f"{line.total_price_rub:.2f}₽\t{line.note}"
|
||||
f"{line.total_price_rub:.2f}₽\t{line.note}\t{line.url or ''}"
|
||||
)
|
||||
rows.append(f"\t\t\t\tИТОГО: {total_cost_rub(lines):.2f}₽\t")
|
||||
rows.append(f"\t\t\t\tИТОГО: {total_cost_rub(lines):.2f}₽\t\t")
|
||||
return "\n".join(rows)
|
||||
|
||||
@@ -30,6 +30,7 @@ from gausse.physics.inductance import (
|
||||
)
|
||||
from gausse.physics.constants import SWITCH_SURGE_FACTOR
|
||||
from gausse.physics.force import saturation_scale, solenoid_field_estimate_tesla
|
||||
from gausse.physics.losses import eddy_reflected_resistance_ohm
|
||||
from gausse.physics.sensors import (
|
||||
inductive_signal_peak_v,
|
||||
make_inductive_sensor_event,
|
||||
@@ -117,16 +118,26 @@ def _ballistic_derivatives(t: float, state: np.ndarray) -> list[float]:
|
||||
return [state[1], 0.0]
|
||||
|
||||
|
||||
def run_stage(
|
||||
entry_x_m: float,
|
||||
entry_v_mps: float,
|
||||
stage: StageConfig,
|
||||
projectile: ProjectileConfig,
|
||||
) -> StageResult:
|
||||
mass_kg = projectile.mass_kg
|
||||
@dataclass(frozen=True)
|
||||
class StagePhysics:
|
||||
"""Физика ступени: индуктивная модель + параметры контура + геометрия.
|
||||
|
||||
Один источник истины для CPU-пути (run_stage) и GPU-батча — оба строят
|
||||
физику через `build_stage_physics`, чтобы числа не расходились.
|
||||
"""
|
||||
|
||||
inductance_model: CoilInductanceModel
|
||||
circuit_params: StageCircuitParams
|
||||
geometry: object
|
||||
r_total_ohm: float
|
||||
r_eddy_coeff_ohm: float
|
||||
capacitance_f: float
|
||||
mu_eff: float
|
||||
|
||||
|
||||
def build_stage_physics(stage: StageConfig, projectile: ProjectileConfig) -> StagePhysics:
|
||||
wire_od_m = stage.wire.insulation_od_mm / 1000
|
||||
geometry = winding_geometry(stage.tube_od_m, wire_od_m, stage.turns_per_layer, stage.layers)
|
||||
|
||||
r_wire = _wire_resistance_ohm(stage.wire, geometry.total_wire_length_m)
|
||||
capacitance_f = stage.capacitor.capacitance_uf * 1e-6
|
||||
l_air_h = air_core_inductance_wheeler(
|
||||
@@ -136,9 +147,18 @@ def run_stage(
|
||||
stage.switch, capacitance_f, l_air_h, stage.charge_voltage_v
|
||||
)
|
||||
r_total_ohm = r_wire + r_switch + stage.capacitor.esr_ohm
|
||||
|
||||
demag = demagnetizing_factor_prolate(projectile.aspect_ratio)
|
||||
mu_eff = effective_permeability(projectile.material.mu_r, demag)
|
||||
char_omega = 1.0 / math.sqrt(l_air_h * capacitance_f)
|
||||
r_eddy_coeff = eddy_reflected_resistance_ohm(
|
||||
slug_radius_m=projectile.diameter_m / 2,
|
||||
slug_length_m=projectile.length_m,
|
||||
slug_resistivity_ohm_m=projectile.material.resistivity_ohm_m,
|
||||
mu_eff=mu_eff,
|
||||
total_turns=geometry.total_turns,
|
||||
coil_length_m=geometry.coil_length_m,
|
||||
char_omega_rad_s=char_omega,
|
||||
)
|
||||
inductance_model = CoilInductanceModel(
|
||||
l_air_h=l_air_h,
|
||||
coil_length_m=geometry.coil_length_m,
|
||||
@@ -148,6 +168,38 @@ def run_stage(
|
||||
total_turns=geometry.total_turns,
|
||||
b_sat_tesla=projectile.material.b_sat_tesla,
|
||||
)
|
||||
circuit_params = StageCircuitParams(
|
||||
capacitance_f=capacitance_f,
|
||||
r_total_ohm=r_total_ohm,
|
||||
mass_kg=projectile.mass_kg,
|
||||
r_eddy_coeff_ohm=r_eddy_coeff,
|
||||
)
|
||||
return StagePhysics(
|
||||
inductance_model=inductance_model,
|
||||
circuit_params=circuit_params,
|
||||
geometry=geometry,
|
||||
r_total_ohm=r_total_ohm,
|
||||
r_eddy_coeff_ohm=r_eddy_coeff,
|
||||
capacitance_f=capacitance_f,
|
||||
mu_eff=mu_eff,
|
||||
)
|
||||
|
||||
|
||||
def run_stage(
|
||||
entry_x_m: float,
|
||||
entry_v_mps: float,
|
||||
stage: StageConfig,
|
||||
projectile: ProjectileConfig,
|
||||
) -> StageResult:
|
||||
mass_kg = projectile.mass_kg
|
||||
phys = build_stage_physics(stage, projectile)
|
||||
geometry = phys.geometry
|
||||
r_total_ohm = phys.r_total_ohm
|
||||
r_eddy_coeff = phys.r_eddy_coeff_ohm
|
||||
capacitance_f = phys.capacitance_f
|
||||
mu_eff = phys.mu_eff
|
||||
inductance_model = phys.inductance_model
|
||||
wire_od_m = stage.wire.insulation_od_mm / 1000
|
||||
|
||||
x_sensor_m = -stage.sensor_to_coil_distance_m
|
||||
|
||||
@@ -192,36 +244,47 @@ def run_stage(
|
||||
x_fire_m = x_at_sensor + v_at_sensor * fire_delay_s
|
||||
v_fire_mps = v_at_sensor
|
||||
|
||||
circuit_params = StageCircuitParams(
|
||||
capacitance_f=capacitance_f,
|
||||
r_total_ohm=r_total_ohm,
|
||||
mass_kg=mass_kg,
|
||||
)
|
||||
circuit_params = phys.circuit_params
|
||||
q0 = capacitance_f * stage.charge_voltage_v
|
||||
# Тиристор проводит ОДИН импульс: разряд обрывается на первом возврате тока
|
||||
# к нулю ИЛИ на первом локальном минимуме тока (снаряд начал подкачивать ток
|
||||
# обратно) — что раньше. Иначе модель гоняла бы катушку несколько раз за
|
||||
# выстрел, что физически невозможно (конденсатор не перезарядить за мкс).
|
||||
discharge_sol = solve_ivp(
|
||||
circuit.derivatives,
|
||||
(0.0, stage.discharge_time_budget_s),
|
||||
[q0, 0.0, x_fire_m, v_fire_mps],
|
||||
args=(inductance_model, circuit_params),
|
||||
events=circuit.zero_current_crossing_event,
|
||||
events=(circuit.zero_current_crossing_event, circuit.current_local_min_event),
|
||||
rtol=1e-8,
|
||||
atol=1e-11,
|
||||
)
|
||||
if len(discharge_sol.t_events[0]) == 0:
|
||||
# какое из событий оборвало разряд первым
|
||||
term_candidates = []
|
||||
for ev_idx in range(2):
|
||||
if len(discharge_sol.t_events[ev_idx]) > 0:
|
||||
term_candidates.append((discharge_sol.t_events[ev_idx][0], discharge_sol.y_events[ev_idx][0]))
|
||||
if not term_candidates:
|
||||
return StageResult(
|
||||
feasible=False,
|
||||
reason="разряд не скоммутировался (ток не вернулся к нулю) в пределах временного бюджета",
|
||||
t_sensor_s=t_sensor_s,
|
||||
t_fire_s=t_sensor_s + fire_delay_s,
|
||||
)
|
||||
|
||||
q_final, i_final, x_final, v_final = discharge_sol.y_events[0][0]
|
||||
term_candidates.sort(key=lambda p: p[0])
|
||||
q_final, i_final, x_final, v_final = term_candidates[0][1]
|
||||
|
||||
energy_in_j = 0.5 * capacitance_f * stage.charge_voltage_v**2
|
||||
energy_remaining_cap_j = q_final**2 / (2 * capacitance_f)
|
||||
energy_dissipated_j = float(
|
||||
np.trapezoid(discharge_sol.y[1] ** 2 * r_total_ohm, discharge_sol.t)
|
||||
)
|
||||
# потери = I²·R_eff(x), где R_eff включает вихревые потери снаряда (× overlap)
|
||||
overlap_during = inductance_model.overlap_fraction(discharge_sol.y[2])
|
||||
r_eff_during = r_total_ohm + r_eddy_coeff * overlap_during
|
||||
resistive_diss = float(np.trapezoid(discharge_sol.y[1] ** 2 * r_eff_during, discharge_sol.t))
|
||||
# при обрыве на минимуме тока в катушке остаётся энергия магнитного поля —
|
||||
# её гасит обратный диод (freewheel), учитываем как потери, чтобы энергия
|
||||
# сходилась. Считаем ТОЧНО (с насыщением), а не ½LI².
|
||||
residual_inductor_j = float(inductance_model.magnetic_energy(x_final, i_final))
|
||||
energy_dissipated_j = resistive_diss + residual_inductor_j
|
||||
kinetic_before_j = 0.5 * mass_kg * v_fire_mps**2
|
||||
kinetic_after_j = 0.5 * mass_kg * v_final**2
|
||||
|
||||
|
||||
45
tests/test_batch_sweep.py
Normal file
45
tests/test_batch_sweep.py
Normal file
@@ -0,0 +1,45 @@
|
||||
import json
|
||||
|
||||
from gausse.components.database import ComponentDatabase
|
||||
from gausse.gpu.batch_sweep import run_gpu_sweep
|
||||
from gausse.optim.search_space import SearchBounds, decode, genome_from_dict
|
||||
from gausse.sim.coilgun import run_coilgun
|
||||
from gausse.storage.database import count_runs, fetch_runs, open_connection
|
||||
|
||||
DB = ComponentDatabase.load()
|
||||
|
||||
|
||||
def test_gpu_sweep_records_all_and_writes_single_stage(tmp_path):
|
||||
db_path = tmp_path / "gpu.sqlite3"
|
||||
summary = run_gpu_sweep(db_path, n_runs=300, seed=3, prefer_gpu=False, batch_size=300)
|
||||
assert summary["n_runs"] == 300
|
||||
conn = open_connection(db_path)
|
||||
assert count_runs(conn) == 300
|
||||
rows = fetch_runs(conn)
|
||||
# GPU-путь одноступенчатый
|
||||
for r in rows:
|
||||
g = genome_from_dict(json.loads(r.genome_json))
|
||||
assert len(g.stages) == 1
|
||||
assert r.search_mode.startswith("gpu-sweep")
|
||||
|
||||
|
||||
def test_gpu_sweep_matches_cpu_within_tolerance(tmp_path):
|
||||
"""Честная сверка: GPU-батч должен давать те же exit_v, что CPU run_coilgun."""
|
||||
db_path = tmp_path / "gpu.sqlite3"
|
||||
run_gpu_sweep(db_path, n_runs=1500, seed=5, prefer_gpu=False, batch_size=1500)
|
||||
conn = open_connection(db_path)
|
||||
bounds = SearchBounds(min_stages=1, max_stages=1)
|
||||
feasible = fetch_runs(conn, feasible=True, order_by_efficiency_desc=True, limit=10)
|
||||
assert len(feasible) >= 3, "нужно несколько реализуемых для сверки"
|
||||
checked = 0
|
||||
for r in feasible:
|
||||
g = genome_from_dict(json.loads(r.genome_json))
|
||||
config, _, _ = decode(g, DB, bounds)
|
||||
cpu = run_coilgun(config)
|
||||
if not cpu.feasible:
|
||||
continue
|
||||
# фикс.шаг батча vs адаптивный solve_ivp + аналитический триггер: ~2%
|
||||
assert abs(r.exit_velocity_mps - cpu.exit_v_mps) <= 0.02 * abs(cpu.exit_v_mps) + 0.1, \
|
||||
f"GPU {r.exit_velocity_mps:.2f} vs CPU {cpu.exit_v_mps:.2f}"
|
||||
checked += 1
|
||||
assert checked >= 3
|
||||
@@ -4,7 +4,7 @@ import pytest
|
||||
|
||||
from gausse.components.database import ComponentDatabase
|
||||
from gausse.optim.search_space import (
|
||||
TUBE_WALL_CLEARANCE_M,
|
||||
PROJECTILE_BORE_CLEARANCE_M,
|
||||
SearchBounds,
|
||||
crossover,
|
||||
decode,
|
||||
@@ -25,8 +25,15 @@ def test_sample_genome_respects_bounds():
|
||||
genome = sample_genome(DB, BOUNDS, rng)
|
||||
assert BOUNDS.min_stages <= len(genome.stages) <= BOUNDS.max_stages
|
||||
assert len(genome.inter_stage_gaps_m) == len(genome.stages) - 1
|
||||
assert BOUNDS.tube_od_m_min <= genome.tube_od_m <= BOUNDS.tube_od_m_max
|
||||
assert genome.projectile.diameter_m <= genome.tube_od_m - TUBE_WALL_CLEARANCE_M
|
||||
assert BOUNDS.tube_inner_d_m_min <= genome.tube_inner_d_m <= BOUNDS.tube_inner_d_m_max
|
||||
assert genome.tube_od_m == pytest.approx(genome.tube_inner_d_m + 2 * genome.tube_wall_m)
|
||||
# снаряд влезает в бор трубы
|
||||
assert genome.projectile.diameter_m <= genome.tube_inner_d_m - PROJECTILE_BORE_CLEARANCE_M + 1e-9
|
||||
# масса снаряда не больше 200 г
|
||||
import math as _m
|
||||
mat = DB.projectile_materials[genome.projectile.material_idx]
|
||||
mass = _m.pi * (genome.projectile.diameter_m / 2) ** 2 * genome.projectile.length_m * mat.density_kg_m3
|
||||
assert mass <= BOUNDS.projectile_mass_max_kg + 1e-6
|
||||
for stage in genome.stages:
|
||||
assert 0 <= stage.wire_idx < len(DB.wires)
|
||||
assert BOUNDS.turns_per_layer_min <= stage.turns_per_layer <= BOUNDS.turns_per_layer_max
|
||||
@@ -53,8 +60,8 @@ def test_mutate_keeps_genome_within_bounds():
|
||||
genome = mutate(genome, DB, BOUNDS, rng, rate=0.5)
|
||||
assert BOUNDS.min_stages <= len(genome.stages) <= BOUNDS.max_stages
|
||||
assert len(genome.inter_stage_gaps_m) == len(genome.stages) - 1
|
||||
assert BOUNDS.tube_od_m_min <= genome.tube_od_m <= BOUNDS.tube_od_m_max
|
||||
assert genome.projectile.diameter_m <= genome.tube_od_m - TUBE_WALL_CLEARANCE_M + 1e-9
|
||||
assert BOUNDS.tube_inner_d_m_min <= genome.tube_inner_d_m <= BOUNDS.tube_inner_d_m_max
|
||||
assert genome.projectile.diameter_m <= genome.tube_inner_d_m - PROJECTILE_BORE_CLEARANCE_M + 1e-9
|
||||
# decode должен всегда успевать без исключений после repair
|
||||
decode(genome, DB, BOUNDS)
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ def _switch(on_resistance_ohm: float = 0.02) -> SwitchSpec:
|
||||
)
|
||||
|
||||
|
||||
def _run(wire, switch, capacitor=CAPACITOR):
|
||||
def _run(wire, switch, capacitor=CAPACITOR, projectile=PROJECTILE):
|
||||
# turns_per_layer выбран так, чтобы длина катушки (~1.7см) была сравнима
|
||||
# со снарядом (2см) — иначе снаряд, войдя глубоко внутрь длинной катушки,
|
||||
# оказывается в плоской зоне перекрытия (dL/dx=0) и сила не действует.
|
||||
@@ -53,7 +53,7 @@ def _run(wire, switch, capacitor=CAPACITOR):
|
||||
sensor_to_coil_distance_m=0.02,
|
||||
charge_voltage_v=350.0,
|
||||
)
|
||||
return run_stage(entry_x_m=-0.05, entry_v_mps=5.0, stage=stage, projectile=PROJECTILE)
|
||||
return run_stage(entry_x_m=-0.05, entry_v_mps=5.0, stage=stage, projectile=projectile)
|
||||
|
||||
|
||||
def test_stage_is_feasible_with_realistic_components():
|
||||
@@ -66,8 +66,16 @@ def test_energy_conserved_exactly_when_lossless():
|
||||
part_number="c-lossless", capacitance_uf=100.0, voltage_v=400.0, esr_ohm=0.0,
|
||||
max_current_a=300.0, price=300.0, source="test",
|
||||
)
|
||||
# снаряд без вихревых потерь: огромное удельное сопротивление -> R_eddy≈0,
|
||||
# чтобы «без потерь» действительно означало отсутствие всех каналов диссипации
|
||||
no_eddy_steel = ProjectileMaterialSpec(
|
||||
name="steel-no-eddy", density_kg_m3=7850.0, mu_r=200.0, b_sat_tesla=1.8,
|
||||
price_per_kg=100.0, source="test", resistivity_ohm_m=1e12,
|
||||
)
|
||||
no_eddy_projectile = ProjectileConfig(material=no_eddy_steel, diameter_m=0.008, length_m=0.02)
|
||||
result = _run(
|
||||
_wire(resistivity_ohm_m=0.0), _switch(on_resistance_ohm=0.0), capacitor=lossless_capacitor
|
||||
_wire(resistivity_ohm_m=0.0), _switch(on_resistance_ohm=0.0),
|
||||
capacitor=lossless_capacitor, projectile=no_eddy_projectile,
|
||||
)
|
||||
assert result.feasible, result.reason
|
||||
assert result.energy_dissipated_j == pytest.approx(0.0, abs=1e-9)
|
||||
|
||||
Reference in New Issue
Block a user