11import json
2- from typing import Dict , List , Tuple , Literal
2+ from typing import Dict , List , Tuple , Literal , Optional
33import pandas as pd
4+ import numpy as np
45from openai import OpenAI
56from huggingface_hub import InferenceClient
67from particleanalyzer .core .language_context import LanguageContext
@@ -17,8 +18,7 @@ def __init__(
1718 ):
1819 self .provider = provider
1920 self .api_key = api_key
20- self .stats : Dict [str , Dict [str , float ]] = {}
21-
21+
2222 if provider == "openrouter" :
2323 self .client = OpenAI (
2424 base_url = "https://openrouter.ai/api/v1" ,
@@ -30,83 +30,138 @@ def __init__(
3030 self .model = huggingface_model
3131 else :
3232 raise ValueError ("Неизвестный провайдер. Доступные варианты: 'openrouter', 'huggingface'" )
33-
34- def load_data (self , df : pd .DataFrame ) -> None :
35- required_columns = {
36- self ._get_translation ("Параметр" ),
37- self ._get_translation ("Среднее" ),
38- self ._get_translation ("Медиана" ),
39- self ._get_translation ("Максимум" ),
40- self ._get_translation ("Минимум" ),
41- self ._get_translation ("Среднее" ),
33+
34+ def _calculate_stats (self , df : pd .DataFrame , num_bins : int = 5 ) -> Dict [str , Dict ]:
35+ """Вычисляет статистику, сохраняя оригинальные названия столбцов"""
36+ stats = {
37+ "particles_count" : len (df ),
38+ "parameters" : {}
4239 }
43- if not required_columns .issubset (df .columns ):
44- raise ValueError (f"DataFrame должен содержать колонки: { required_columns } " )
45-
46- self .stats = {
47- row [self ._get_translation ("Параметр" )]: {
48- self ._get_translation ("Среднее" ): row [self ._get_translation ("Среднее" )],
49- self ._get_translation ("Медиана" ): row [self ._get_translation ("Медиана" )],
50- self ._get_translation ("Максимум" ): row [self ._get_translation ("Максимум" )],
51- self ._get_translation ("Минимум" ): row [self ._get_translation ("Минимум" )],
52- self ._get_translation ("Среднее" ): row [self ._get_translation ("Среднее" )],
40+
41+ numeric_cols = df .select_dtypes (include = [np .number ]).columns
42+
43+ for col in numeric_cols :
44+ # Базовые статистики
45+ stats ["parameters" ][col ] = {
46+ "mean" : float (df [col ].mean ()),
47+ "median" : float (df [col ].median ()),
48+ "std" : float (df [col ].std ()),
49+ "min" : float (df [col ].min ()),
50+ "max" : float (df [col ].max ()),
51+ "q1" : df [col ].quantile (0.25 ),
52+ "q3" : df [col ].quantile (0.75 ),
53+ "skewness" : df [col ].skew (),
54+ "kurtosis" : df [col ].kurtosis (),
55+ "histogram" : self ._create_histogram (df [col ], num_bins )
5356 }
54- for _ , row in df .iterrows ()
57+
58+ # Специальные расчеты (по оригинальным названиям)
59+ if "Dₘₐₓ" in col and any ("Dₘᵢₙ" in c for c in df .columns ):
60+ stats ["parameters" ]["aspect_ratio" ] = self ._calc_aspect_ratio (df , num_bins )
61+
62+ if "P [" in col and any ("S [" in c for c in df .columns ):
63+ stats ["parameters" ]["circularity" ] = self ._calc_circularity (df , num_bins )
64+ return stats
65+
66+ def _create_histogram (self , data , num_bins ):
67+ """Создает гистограмму"""
68+ counts , bins = np .histogram (data , bins = num_bins )
69+ return {
70+ "bins" : [float (x ) for x in bins ],
71+ "counts" : [int (x ) for x in counts ]
5572 }
5673
57- def analyze (self , df : pd .DataFrame , count_particles : int ) -> List [Tuple [None , str ]]:
58- self .count_particles = count_particles
59- if df .empty :
60- return [(None , None )]
74+ def _calc_aspect_ratio (self , df , num_bins ):
75+ """Вычисляет аспектное соотношение"""
76+ dmax_col = [c for c in df .columns if "Dₘₐₓ" in c ][0 ]
77+ dmin_col = [c for c in df .columns if "Dₘᵢₙ" in c ][0 ]
78+ ar = df [dmax_col ] / df [dmin_col ]
79+ return {
80+ "mean" : float (ar .mean ()),
81+ "median" : float (ar .median ()),
82+ "histogram" : self ._create_histogram (ar , num_bins )
83+ }
84+
85+ def _calc_circularity (self , df , num_bins ):
86+ """Вычисляет округлость"""
87+ p_col = [c for c in df .columns if "P [" in c ][0 ]
88+ s_col = [c for c in df .columns if "S [" in c ][0 ]
89+ circ = 4 * np .pi * df [s_col ] / (df [p_col ] ** 2 )
90+ return {
91+ "mean" : float (circ .mean ()),
92+ "median" : float (circ .median ()),
93+ "histogram" : self ._create_histogram (circ , num_bins )
94+ }
95+
96+ def analyze (self , df : pd .DataFrame ) -> List [Tuple [None , str ]]:
97+ """Анализирует DataFrame с частицами и возвращает результаты LLM"""
6198 self .lang = LanguageContext .get_language ()
62- self .load_data (df )
99+
100+ if df .empty :
101+ return [(None , "No particles detected" )]
102+
103+ # Вычисляем статистику
104+ stats = self ._calculate_stats (df )
105+ count_particles = len (df )
106+
63107 try :
64- prompt = self ._build_prompt ()
108+ prompt = self ._build_prompt (stats , count_particles )
65109 response = self ._get_llm_response (prompt )
66110 return self ._format_response (response )
67111 except Exception as e :
68112 print (f"LLM analysis failed: { str (e )} " )
69- return [(None , None )]
113+ return [(None , f"Analysis error: { str ( e ) } " )]
70114
71- def _build_prompt (self ) -> str :
115+ def _build_prompt (self , stats , count_particles ) -> str :
72116 return f"""
73- Ты — эксперт в области материаловедения и сканирующей электронной микроскопии с 10 летним опытом.
74- Проведи интерпретацию изображения СЭМ на основе статистических данных о размерных характеристиках частиц.
75- В таблице представлены следующие характеристики:
76- - Геометрические параметры: площадь, периметр, эквивалентный диаметр, диаметр Ферета (min, max, mean)
77- - Морфологические параметры: эксцентриситет
78- - Ориентация: максимальный/минимальный угол диаметра Ферета
79- - Средняя интенсивность пикселей частиц
80- При анализе больше обращай внимание на среднее и медианное значение, а не на максимальное и минимальное значение, так туда могут попасть обрезанные частицы.
81-
82- 🔍 **Сфокусируйся на следующих аспектах:**
83- 1. **Размерное распределение** — структура распределения, наличие преобладающих фракций, разброс (SD), соотношение Max/Min.
84- Обрати внимания что результаты могут быть представлены как в мкм так и в пикселях.
85- 2. **Морфология** — эксцентриситет, округлость, вытянутость (Dₘₐₓ / Dₘᵢₙ)
86- 3. **Ориентация частиц** — есть ли выраженное предпочтение в ориентации.
87-
88- 📋 **Полная таблица данных:**
89- Всего на изображении обнаружено { self .count_particles } частиц. Со следующими характеристиками:
90- { json .dumps (self .stats , indent = 2 )}
91-
92- ✍️ **Сформулируй анализ в следующем формате:**
93-
94- 🔬 **Микроструктурный анализ**:
95- - Размер частиц: <анализ по D, SD, разброс, количество частиц>
96- - Форма: <анализ по e, аспектному отношению>
97- - Ориентация: <анализ распределения углов>
98-
99- 🧪 **Материаловедческие выводы**:
100- - <влияние морфологии и распределения на свойства материала, возможное происхождение>
101-
102- 💡 **Рекомендации**:
103- - <предложения по улучшению технологии, методы контроля качества, возможные причины аномалий>
104-
105- Отвечай на следующем языке { self .lang }
117+ Ты — ведущий эксперт в материаловедении и сканирующей электронной микроскопии (СЭМ) с 15-летним стажем.
118+ Твоя задача — подготовить развёрнутый экспертный отчёт по характеристикам { count_particles } частиц.
119+
120+ 📌 **Контекст**:
121+ - Данные представлены в агрегированном виде (средние значения, медианы, стандартные отклонения и др.).
122+ - Размеры могут быть в микрометрах или пикселях.
123+ - Возможны аномально низкие значения из-за обрезки частиц по краям изображения.
124+ - **Важно**: Крупные частицы считаются достоверными — **не упоминай влияние обрезки в их отношении**.
125+ - Не пересказывай входные данные — **только интерпретируй**
126+ - Опирайся на количественные показатели и сравнение между параметрами. Избегай общих фраз без числовой поддержки. Анализ должен быть научно обоснованным и лаконичным.
127+
128+ 🔬 **Задачи анализа**:
129+ 1. **Распределение размеров** — тип, фракции, аномалии, полидисперсность
130+ 2. **Морфология** — форма, вытянутость, однородность
131+ 3. **Ориентация** — направленность
132+ 4. **Проблемные зоны** — участки с сомнительными измерениями
133+
134+ ✍️ **Формат отчёта (язык: { self .lang } )** — без лишних отступов, используй маркированные списки и смайлики-иконки:
135+
136+ 🌡️ **Размерные характеристики**:
137+ - **Размер**: <диаметр, диапазоны, преобладающие фракции>
138+ - **Тип распределения**: <нормальное / бимодальное / асимметричное>
139+ - **Аномалии**: <выбросы, артефакты, влияние обрезки>
140+ - **Полидисперсность**: <низкая / умеренная / высока
141+
142+ 🔵 **Морфология**:
143+ - **Преобладающая форма**: <основано на circularity, aspect_ratio, эксцентриситете e>
144+ - **Однородность**: <оценка схожести форм>
145+ - **Особенности**: <необычные формы, группы, дефекты>
146+
147+ 🧭 **Ориентация**:
148+ - **Тип распределения углов**: <равномерное / направленное>
149+ - **Предпочтительные направления**: <если есть>
150+
151+ ⚠️ **Проблемные зоны**:
152+ - <опиши участки с сомнительными или недостоверными измерениями>
153+
154+ 📊 Выводы:
155+ - Используй числовые значения с точностью до 2 знаков после запятой
156+ - Выводы должны быть лаконичными и научно обоснованными
157+ - Рекомендации не требуются
158+
159+ 📁 **Данные для анализа** (агрегированные показатели):
160+ { json .dumps (stats , indent = 2 )}
106161 """
107162
108163 def _get_llm_response (self , prompt : str ):
109- """Отправка запроса в зависимости от провайдера """
164+ """Отправка запроса в LLM """
110165 if self .provider == "openrouter" :
111166 completion = self .client .chat .completions .create (
112167 extra_headers = {
@@ -126,12 +181,9 @@ def _get_llm_response(self, prompt: str):
126181 )
127182
128183 def _format_response (self , response ) -> List [Tuple [None , str ]]:
129- """Форматирование ответа для обоих провайдеров """
184+ """Форматирование ответа LLM """
130185 if self .provider == "openrouter" :
131186 analysis = response .choices [0 ].message .content
132187 elif self .provider == "huggingface" :
133188 analysis = response .choices [0 ].message .content
134- return [(None , analysis )]
135-
136- def _get_translation (self , text ):
137- return translations .get (self .lang , {}).get (text , text )
189+ return [(None , analysis )]
0 commit comments