From cc56475d7905f4bd60080fadc823e63f658735d8 Mon Sep 17 00:00:00 2001 From: Artemiy Date: Thu, 7 May 2026 14:22:14 +0500 Subject: [PATCH] =?UTF-8?q?=D0=9F=D1=80=D0=BE=D0=B3=D1=80=D0=B0=D0=BC?= =?UTF-8?q?=D0=BC=D0=B0=20=D0=BF=D0=B5=D1=80=D0=B5=D0=BA=D0=BE=D0=BC=D0=BF?= =?UTF-8?q?=D0=B0=D0=BD=D0=BE=D0=B2=D0=B0=D0=BD=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .1.txt.kate-swp | Bin 0 -> 13669 bytes .experiment_design.py.kate-swp | Bin 0 -> 78 bytes .gitignore | 2 + README.md | 74 +- main.py | 12 +- model.py | 192 --- patch.sh | 1247 +++++++++++++++++ reagent.py | 7 - requirements.txt | 1 + run.sh | 2 + src/__init__.py | 3 + src/controllers/__init__.py | 4 + src/controllers/experiment_controller.py | 23 + .../controllers/medium_controller.py | 170 +-- src/models/__init__.py | 5 + src/models/experiment_model.py | 80 ++ src/models/medium_model.py | 113 ++ src/models/reagent.py | 10 + src/utils/__init__.py | 3 + src/views/__init__.py | 5 + .../views/experiment_view.py | 345 +---- main_window.py => src/views/main_window.py | 114 +- src/views/medium_view.py | 207 +++ view.py | 422 ------ 24 files changed, 1873 insertions(+), 1168 deletions(-) create mode 100644 .1.txt.kate-swp create mode 100644 .experiment_design.py.kate-swp delete mode 100644 model.py create mode 100644 patch.sh delete mode 100644 reagent.py create mode 100755 run.sh create mode 100644 src/__init__.py create mode 100644 src/controllers/__init__.py create mode 100644 src/controllers/experiment_controller.py rename controller.py => src/controllers/medium_controller.py (55%) create mode 100644 src/models/__init__.py create mode 100644 src/models/experiment_model.py create mode 100644 src/models/medium_model.py create mode 100644 src/models/reagent.py create mode 100644 src/utils/__init__.py create mode 100644 src/views/__init__.py rename experiment_design.py => src/views/experiment_view.py (64%) rename main_window.py => src/views/main_window.py (53%) create mode 100644 src/views/medium_view.py delete mode 100644 view.py diff --git a/.1.txt.kate-swp b/.1.txt.kate-swp new file mode 100644 index 0000000000000000000000000000000000000000..c33faae3e4198344ab87e2934ce7d3dc2c1a5409 GIT binary patch literal 13669 zcmYk?b<|W>k+P0tM451uc4#=I92t2T7s#I!MDhRsd!VxwbLdD> zM^au^#F36D>_|=&bm#;H97#Bm-;tTf>&Qgpaik@3J2X|f!tcrHh~s5(4h<)J_((QK z0bZ8HQH#jzNK9mMXlxlB={b_Yk&Q^_(DkGZzbuVI15NEH!+TOW3K1zCnyM6zaU4nR zm`o&dOd(>!k<>AbBS{?NiNucVL?VYKF`=U-M-n)46Y;|@i|5d$h&p0Ky zb+C7$PaN#t=synTE&AAzh4{-+mU!e~;-e28%vlt1-yHcl za?in)M(;X`5_cRdN%XdZd5hk16eeyuG=durea>}Co9u zI5g1X4lVF8M@8Oq)S=Hg;?Rs9c4+CpcI4$fha6hdgAT3SR}RhFmkurC7Y<$a=Z@x_ z?Nf(F{fR@XeZY~6m+g0CAohjhBZuCz*TIuKy2rt@D!M!To?Q;@gPjg-iQ*?zx+kd%3!&&CgYAATr&A}TE z-HnSJTJ41nedq#*w%B|}OV0LM_{h9)%yrb`WpfX9a`Yg z4sG&L4&BZp!!g34$Hs7n*7OBOYfk;VLpSX(ht4+Cq3Ips$jN&KJG83?Idq#1bkyNx z&pNcG101@9XB>L&^mFKQ`Z{!L_Hk&#_I8xx)V&h4zY3$H4HgafJHFS(%I1L<{(fW>79I5BfcB$*ogw%FuptT&@NHrb0 z(QAaGx8>w(O3WcL!_&o(2HJJGP4oy`)hZZZZLkpb8p;gEo zK9b9!n<>tr@A@1Lt#)>Y<{+Cx@5$=W(r0mKdNVt;uQNGx>Wtx+WpL=3nBJi!N$1da zR$7Oi{%IVgn1j>~&2=h=hLbYrXno>2G=iu@+cM(NE#tojTxsM#7poBY*TE`8o;Y~mM;?dcZ$~Ug{&MguAo9pj zm3Zi2IFUacvxz?(gNWZ9i--pfZnMaJ2hW|zZw?LgS4U-z+;i|mirjT*)OQ@LT;#Tc zC5im(;MY^+mO}%*>FB|c8;;(@bqDJcx#r+Lja+ra5I;J2jz+FH*kX~(j-kXQM^ECS zL-TgQ!48R>ceE$YIk@K{KR7xOXB~}+GY(z$_YST0cMgsEv_sc(%ApxO>Cl>TE=f2+_aHH4$bvJhj!vuj$XX%ONUPIg@d0s zkxKqh_H)+kb&Wb3NaoQ@`eD z&4d;P{?BLf^ zWRgQyIx!qCIy7$+99r%1j@q1HoI@iR*rA8_AV*7H_MAgIexO57z5$LHUe@2C+pM3XJV*LE_~{Yp@;^QEE+Z1o&k z#<~toZykreBx*afLuxv-cd9#dwrY-AjJj(0NEJs3j#PFGCMr7gITajrIa1!C@2ql; z`Wz|a&2ji5yMNO6ZoP|UG{vlVe@#}{@~=SU%k=DMImkDB}rjUcZ>mypMy z2V`!Cwtp^%Zl;_LJ^A7sx}9@4v|+P3ba7c7I&~I@t|zlY1I^@!;}SADbeCjsWaUVD zhjx5ghrS!rICOET9om+u9J+**4sE^^4(*U+4vjiC97)43OX7%SIEfv)xI_+p|0HxQ z=4A;SBZ>G9?cR6}4K&7~`!wp%@6m`u1NGknP7wU-;e}I;Ep31@r#2yE4c08 zt`B~8=yPs4_%aG^IyAN$j&vNk?qG`r*Bl!4PY&ifxa!D6{ODkd1y>y0Ou=ObUv0r9 zM`PlmgL@&k;Lyv?J9vr(=NxQ`;0K2Wde+g7Bj1OQeCJ?~2B#ew=qU%AA~@;L^_*~M zLXJDQF@j@`RK!t-UUtO6?hU?m@VyWmc5rV8-#GZ53%+(}^Br<%pa&g1`GT(;+)Tlj z4vpXoheq(ZLnHXi!F?Kh>fl=@_{5=QJmBchk^K&S8U`ObG`4*X-3uQ%v`6-ZL!;j4(9&;k z=>A#nNY4n?IW&T`4vl(^Lo>SCp$+@CL$~HChil#Xork* zXrN;qZF$)khvsdxL!UFsp@EKcq~tyS`{B%Qq~HY~sYMKPXu}S5Xu}Q($6!ZO-ZRKi zpBU)S#XalLj1F+G@QHq!KanvSyJG2jaIWz}79eM=xaOirv zJM^Az4&CTo9QvHj4*s1LbaH6BbaZHJ9UQvs_70u8okR21#-XnQ&d8tM{H3M-8U(t3 zS~zrZ%^doirVj1ICXTe6ps_(K5k?a<1Va%iqg zI+Aj>5)R$<#T}aKqK+6|R>Yx)cOi!!b_E?;(*h2CX#Vh#d=8Buk3*-{hb_f634YE13ud{}aWO4N1WtknC#7qvoC!<3vm)@ZzN$1e#qzxZQaf|5`IibF9d3PqqW6oP_K z0P;gV$P0NOH{^nx5C=IRJ7j~bkOeYBCdddGAU&jmw2%f;Ln=rKnuFy0Eg8f@Qb+=c zArT~m1P~wMK@9Le;l)HCcuMp?_!pkQKkyj-hQHtuJcK{t5BMD(z za2tMxTW}L@z;(C=KfzV_5w5^xxC9sB0-T3)@B^HMGw?lp2dCi_oP-l_9FD#z_OzC7nOK1Vj zp&2xVCeRofK|^Q&^`Rcrg*s3hYC%n?0o9=zRD~)~87e_Vr~u`m9F&DJP#Q`>Nhkru zp%@f}B2X9#K|v@0`5_?1PVBFYJNcunRtfo$vwdfbH-; zY=ie;D{O(yun9K823Qa8!a7(BYhX3J18>7BSP3g&IV^*v@D?nA#qcJ)0gK>uSO^PX zKD-9=U@p80b6_^if>&TBybLp7I=lqaU@A<3$uJ2f!iz8g#=|%m3u9n3jDnFc0*1p2 z@H`BIp)dpn!ytGL2Ewy20Q$o-&=2}TALtFepeOWz?$8aoLKo-^ouDIhfcDT1+Cm#> z4XvOhw1DQ&44Og{Xbg>@AvA#cP!H-t9jFbppeEFS>QD`;LKUbCm7pS2fbvid%0d|^ z4W*zYlz`$;42nV#C=7+5AQXW7kPq@g9>@*3ASc8@4#*DKAS+~n%#aB(LIy|==^!nn zfz*%+QbGzy4#^-El0p(l42d8iB!Ku34`Lt+5eS|V{0INS6Zi)n!{6{1Jc5VtC;S1w z!vnYvzrn9?5AMPp_yumm&u|NF!VS0%*Wf3(3O~XXxD1!zB3yv;a1MTevv3B!hwtDt zoPv{Z0*=EmI0{GLTR05gz}IjH4#HRPC42#&!)Nd*d;$kxKYR@P;3L=zdtf*0f)8OQ zd;mLOJG>9u;62z1TVOM6f{m~N*2BB74%WgNSPk#M+pr2&!U|Xp%U~(I1xsKtya{i> zB6uAZ!UC8NufaT+3$MZ)m<_Yw6_^Px!wi@XFTpgJ3R7S*OoEBAT7#Izs zU?hxy;qU@H55r(641vKg2%dw1@GJ~~{_qU+gTBxQdP6Vh2|b`Ybc3$Y1v*0~=m;I4 zJ+yM#89{%@7?~K&_KlCYfYybcN literal 0 HcmV?d00001 diff --git a/.experiment_design.py.kate-swp b/.experiment_design.py.kate-swp new file mode 100644 index 0000000000000000000000000000000000000000..3ec3847ebe92b3ebfe5065aa01ad945a9c5b1e86 GIT binary patch literal 78 zcmZQzU=Z?7EJ;-eE>A2_aLdd|RWQ;sU|?Vn*} float: - """ - Пересчитывает количество из базовых единиц (мкл/мкг) в целевую единицу. - """ - if is_volume: - conversion_factor = VOLUME_UNITS.get(target_unit, 1.0) - else: - conversion_factor = MASS_UNITS.get(target_unit, 1.0) - - return amount_base / conversion_factor - - def calculate_amounts(self) -> tuple: - """ - Рассчитывает: - 1. Исходное количество реагента для неразбавленного состава - 2. Количество разбавленного реагента, которое нужно взять - 3. Количество растворителя с учётом добавленных разбавленных реагентов - - Возвращает: (список количеств разбавленных реагентов, количество растворителя, процент растворителя) - """ - results = [] - - # Проверяем, есть ли реагенты - if not self.reagents: - return results, self.total_amount, 100.0 - - # Суммируем проценты всех реагентов - total_percentage = sum(reagent.percentage for reagent in self.reagents) - - - # Проверяем, что сумма процентов не превышает 100 - if total_percentage > 100: - raise ValueError(f"Сумма процентов реагентов ({total_percentage:.2f}%) превышает 100%") - - # Шаг 1: Рассчитываем общий объём среды в базовых единицах (мкл) - total_in_base = self.total_amount * VOLUME_UNITS[self.amount_unit] - - # Шаг 2: Рассчитываем количество каждого реагента для неразбавленного состава - undiluted_amounts = [] - for reagent in self.reagents: - - # Расчёт количества реагента в базовых единицах - amount_in_base = (reagent.percentage / 100) * total_in_base - - # Определение типа единицы измерения реагента - is_volume = reagent.unit in VOLUME_UNITS - - # Применение коэффициента пересчёта - adjusted_amount = amount_in_base * reagent.conversion_factor - - # Пересчёт в целевую единицу измерения - final_amount = self.convert_amount(adjusted_amount, reagent.unit, is_volume) - - undiluted_amounts.append(final_amount) - - # Шаг 3: Рассчитываем количество разбавленного реагента, которое нужно взять - # и общий объём вносимых разбавленных реагентов (в базовых единицах) - diluted_amounts = [] - total_diluted_volume_base = 0 - - for i, reagent in enumerate(self.reagents): - dilution_factor = getattr(reagent, 'dilution_factor', 1.0) - - if dilution_factor <= 0: - dilution_factor = 1.0 - - # Количество разбавленного реагента = исходное количество / фактор разбавления - diluted_amount = undiluted_amounts[i] * dilution_factor - - diluted_amounts.append(diluted_amount) - - # Вычисляем объём разбавленного реагента в базовых единицах (мкл) - is_volume = reagent.unit in VOLUME_UNITS - if is_volume: - # Если реагент в объёмных единицах, его объём = diluted_amount - reagent_volume_base = diluted_amount * VOLUME_UNITS[reagent.unit] - else: - # Если реагент в массовых единицах, считаем, что он вносит пренебрежимый объём - # или можно добавить коэффициент плотности, пока игнорируем - reagent_volume_base = 0 - - total_diluted_volume_base += reagent_volume_base - - # Шаг 4: Рассчитываем количество растворителя - # Растворитель = общий объём - объём всех разбавленных реагентов - solvent_volume_base = total_in_base - total_diluted_volume_base - - # Переводим количество растворителя в целевую единицу измерения - solvent_amount = solvent_volume_base / VOLUME_UNITS[self.amount_unit] - - # Если объём реагентов превышает общий объём, корректируем - if solvent_amount < 0: - solvent_amount = 0 - - # Процент растворителя в неразбавленном составе - solvent_percentage = 100 - total_percentage - - return diluted_amounts, solvent_amount, solvent_percentage - - def save_to_file(self, filename: str): - """Сохраняет модель в JSON-файл""" - data = { - 'total_amount': self.total_amount, - 'amount_unit': self.amount_unit, - 'solvent': self.solvent, - 'reagents': [ - { - 'name': r.name, - 'percentage': r.percentage, - 'unit': r.unit, - 'conversion_factor': r.conversion_factor, - 'dilution_factor': getattr(r, 'dilution_factor', 1.0) - } for r in self.reagents - ] - } - with open(filename, 'w', encoding='utf-8') as f: - json.dump(data, f, ensure_ascii=False, indent=4) - - def load_from_file(self, filename: str): - """Загружает модель из JSON-файла""" - with open(filename, 'r', encoding='utf-8') as f: - data = json.load(f) - - self.total_amount = data['total_amount'] - self.amount_unit = data['amount_unit'] - self.solvent = data['solvent'] - self.reagents.clear() - - for r_data in data['reagents']: - reagent = Reagent( - name=r_data['name'], - percentage=r_data['percentage'], - unit=r_data['unit'], - conversion_factor=r_data.get('conversion_factor', 1.0) - ) - # Добавляем коэффициент разбавления - reagent.dilution_factor = r_data.get('dilution_factor', 1.0) - self.reagents.append(reagent) - - def add_reagent(self, name: str, percentage: float, unit: str, conversion_factor: float = 1.0, - dilution_factor: float = 1.0): - """Добавляет новый реагент в модель""" - reagent = Reagent(name, percentage, unit, conversion_factor) - reagent.dilution_factor = dilution_factor - self.reagents.append(reagent) - - def remove_reagent(self, index: int): - """Удаляет реагент по индексу""" - if 0 <= index < len(self.reagents): - del self.reagents[index] - - def clear_reagents(self): - """Очищает список реагентов""" - self.reagents.clear() - - def get_reagent_count(self) -> int: - """Возвращает количество реагентов в модели""" - return len(self.reagents) - - def set_total_amount(self, amount: float, unit: str): - """Устанавливает общее количество среды и единицу измерения""" - self.total_amount = amount - self.amount_unit = unit - - def set_solvent(self, solvent_name: str): - """Устанавливает название растворителя""" - self.solvent = solvent_name diff --git a/patch.sh b/patch.sh new file mode 100644 index 0000000..44ae0fa --- /dev/null +++ b/patch.sh @@ -0,0 +1,1247 @@ +#!/bin/bash + +# Цветной вывод +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +echo -e "${GREEN}=== Реорганизация программы Цифровой помощник биохимика ===${NC}" + +# Создаём бэкап текущей версии +BACKUP_DIR="backup_$(date +%Y%m%d_%H%M%S)" +echo -e "${YELLOW}Создание бэкапа в $BACKUP_DIR...${NC}" +mkdir -p "$BACKUP_DIR" +cp *.py "$BACKUP_DIR/" 2>/dev/null +echo -e "${GREEN}✓ Бэкап создан${NC}" + +# Создаём структуру папок +echo -e "${YELLOW}Создание структуры проекта...${NC}" +mkdir -p src/models +mkdir -p src/views +mkdir -p src/controllers +mkdir -p src/utils + +# Файл: src/models/reagent.py +cat > src/models/reagent.py << 'EOF' +class Reagent: + def __init__(self, name: str, percentage: float, unit: str, conversion_factor: float = 1.0): + self.name = name + self.percentage = percentage + self.unit = unit + self.conversion_factor = conversion_factor + self.dilution_factor = 1.0 + + def __repr__(self): + return f"Reagent(name={self.name}, percentage={self.percentage}, unit={self.unit})" +EOF + +# Файл: src/models/__init__.py +cat > src/models/__init__.py << 'EOF' +from .reagent import Reagent +from .medium_model import MediumModel +from .experiment_model import ExperimentModel + +__all__ = ['Reagent', 'MediumModel', 'ExperimentModel'] +EOF + +# Файл: src/models/medium_model.py +cat > src/models/medium_model.py << 'EOF' +import json +from typing import List, Tuple +from .reagent import Reagent + +VOLUME_UNITS = { + 'нл': 0.001, 'мкл': 1.0, 'мл': 1000.0, 'л': 1000000.0 +} + +MASS_UNITS = { + 'нг': 0.000001, 'мкг': 0.001, 'мг': 1.0, 'г': 1000.0, 'кг': 1000000.0 +} + +class MediumModel: + def __init__(self): + self.total_amount = 100.0 + self.amount_unit = 'мл' + self.solvent = 'Вода' + self.reagents = [] + + def convert_amount(self, amount_base: float, target_unit: str, is_volume: bool) -> float: + if is_volume: + conversion_factor = VOLUME_UNITS.get(target_unit, 1.0) + else: + conversion_factor = MASS_UNITS.get(target_unit, 1.0) + return amount_base / conversion_factor + + def calculate_amounts(self) -> Tuple[List[float], float, float]: + results = [] + if not self.reagents: + return results, self.total_amount, 100.0 + + total_percentage = sum(r.percentage for r in self.reagents) + if total_percentage > 100: + raise ValueError(f"Сумма процентов реагентов ({total_percentage:.2f}%) превышает 100%") + + total_in_base = self.total_amount * VOLUME_UNITS[self.amount_unit] + + undiluted_amounts = [] + for reagent in self.reagents: + amount_in_base = (reagent.percentage / 100) * total_in_base + is_volume = reagent.unit in VOLUME_UNITS + adjusted_amount = amount_in_base * reagent.conversion_factor + final_amount = self.convert_amount(adjusted_amount, reagent.unit, is_volume) + undiluted_amounts.append(final_amount) + + diluted_amounts = [] + total_diluted_volume_base = 0 + for i, reagent in enumerate(self.reagents): + dilution_factor = getattr(reagent, 'dilution_factor', 1.0) + if dilution_factor <= 0: + dilution_factor = 1.0 + diluted_amount = undiluted_amounts[i] * dilution_factor + diluted_amounts.append(diluted_amount) + is_volume = reagent.unit in VOLUME_UNITS + if is_volume: + reagent_volume_base = diluted_amount * VOLUME_UNITS[reagent.unit] + else: + reagent_volume_base = 0 + total_diluted_volume_base += reagent_volume_base + + solvent_volume_base = total_in_base - total_diluted_volume_base + solvent_amount = solvent_volume_base / VOLUME_UNITS[self.amount_unit] + if solvent_amount < 0: + solvent_amount = 0 + solvent_percentage = 100 - total_percentage + + return diluted_amounts, solvent_amount, solvent_percentage + + def save_to_file(self, filename: str): + data = { + 'total_amount': self.total_amount, + 'amount_unit': self.amount_unit, + 'solvent': self.solvent, + 'reagents': [{'name': r.name, 'percentage': r.percentage, 'unit': r.unit, + 'conversion_factor': r.conversion_factor, 'dilution_factor': getattr(r, 'dilution_factor', 1.0)} + for r in self.reagents] + } + with open(filename, 'w', encoding='utf-8') as f: + json.dump(data, f, ensure_ascii=False, indent=4) + + def load_from_file(self, filename: str): + with open(filename, 'r', encoding='utf-8') as f: + data = json.load(f) + self.total_amount = data['total_amount'] + self.amount_unit = data['amount_unit'] + self.solvent = data['solvent'] + self.reagents.clear() + for r_data in data['reagents']: + reagent = Reagent(r_data['name'], r_data['percentage'], r_data['unit'], r_data.get('conversion_factor', 1.0)) + reagent.dilution_factor = r_data.get('dilution_factor', 1.0) + self.reagents.append(reagent) + + def add_reagent(self, name: str, percentage: float, unit: str, conversion_factor: float = 1.0, dilution_factor: float = 1.0): + reagent = Reagent(name, percentage, unit, conversion_factor) + reagent.dilution_factor = dilution_factor + self.reagents.append(reagent) + + def remove_reagent(self, index: int): + if 0 <= index < len(self.reagents): + del self.reagents[index] + + def clear_reagents(self): + self.reagents.clear() + + def get_reagent_count(self) -> int: + return len(self.reagents) + + def set_total_amount(self, amount: float, unit: str): + self.total_amount = amount + self.amount_unit = unit + + def set_solvent(self, solvent_name: str): + self.solvent = solvent_name +EOF + +# Файл: src/models/experiment_model.py +cat > src/models/experiment_model.py << 'EOF' +import numpy as np +from typing import List, Dict, Any + +class ExperimentModel: + def __init__(self): + self.factors = [] + self.responses = [] + self.center_points = 3 + self.randomize = True + + def set_factors(self, factors: List[Dict]): + self.factors = factors + + def set_responses(self, responses: List[Dict]): + self.responses = responses + + def set_center_points(self, n: int): + self.center_points = n + + def set_randomize(self, value: bool): + self.randomize = value + + def calculate_factorial_design(self) -> List[Dict]: + k = len(self.factors) + if k == 0: + return [] + n_factorial = 2 ** k + design = [] + for i in range(n_factorial): + experiment = {} + for j in range(k): + coded_level = -1 if (i >> (k - 1 - j)) & 1 == 0 else 1 + natural_value = self.factors[j]['low'] if coded_level == -1 else self.factors[j]['high'] + experiment[f"Фактор_{j+1}"] = { + 'coded': coded_level, + 'natural': natural_value, + 'name': self.factors[j]['name'], + 'unit': self.factors[j]['unit'] + } + design.append(experiment) + for i in range(self.center_points): + center_experiment = {} + for j in range(k): + center_experiment[f"Фактор_{j+1}"] = { + 'coded': 0, + 'natural': self.factors[j]['center'], + 'name': self.factors[j]['name'], + 'unit': self.factors[j]['unit'] + } + center_experiment['is_center'] = True + center_experiment['center_num'] = i + 1 + design.append(center_experiment) + if self.randomize: + import random + random.shuffle(design) + return design + + def analyze_results(self, results: List[List[float]], design: List[Dict]) -> Dict: + analysis = {} + for resp_idx, response in enumerate(self.responses): + resp_name = response.get('name', f'Отклик_{resp_idx+1}') + y_values = [results[i][resp_idx] for i in range(len(results))] + mean_y = np.mean(y_values) + variance = np.var(y_values, ddof=1) if len(y_values) > 1 else 0 + std_dev = np.std(y_values, ddof=1) if len(y_values) > 1 else 0 + cv = (std_dev / mean_y) * 100 if mean_y != 0 else 0 + factorial_y = [] + center_y = [] + for i, exp in enumerate(design): + if exp.get('is_center', False): + center_y.append(y_values[i]) + else: + factorial_y.append(y_values[i]) + center_variance = np.var(center_y, ddof=1) if len(center_y) > 1 else 0 + analysis[resp_name] = { + 'mean': mean_y, 'variance': variance, 'std_dev': std_dev, 'cv': cv, + 'factorial_values': factorial_y, 'center_values': center_y, 'center_variance': center_variance, + 'n_factorial': len(factorial_y), 'n_center': len(center_y) + } + return analysis +EOF + +# Файл: src/views/__init__.py +cat > src/views/__init__.py << 'EOF' +from .main_window import MainWindow +from .medium_view import MediumCalculatorWindow +from .experiment_view import ExperimentDesignWindow + +__all__ = ['MainWindow', 'MediumCalculatorWindow', 'ExperimentDesignWindow'] +EOF + +# Файл: src/views/main_window.py +cat > src/views/main_window.py << 'EOF' +from PyQt5.QtWidgets import (QMainWindow, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QWidget, QFrame) +from PyQt5.QtCore import Qt +from PyQt5.QtGui import QFont + +class MainWindow(QMainWindow): + def __init__(self): + super().__init__() + self.setWindowTitle("Цифровой помощник биохимика - Главное меню") + self.setGeometry(300, 200, 700, 500) + self.setStyleSheet(""" + QMainWindow { background-color: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #e8f4f8, stop:1 #f0f0f0); } + QPushButton { background-color: #2196F3; color: white; border: none; padding: 15px; font-size: 16px; font-weight: bold; border-radius: 8px; } + QPushButton:hover { background-color: #1976D2; } + QLabel { color: #333; font-size: 14px; } + """) + self._init_ui() + self.medium_calculator = None + self.experiment_window = None + + def _init_ui(self): + central_widget = QWidget() + self.setCentralWidget(central_widget) + layout = QVBoxLayout(central_widget) + layout.setSpacing(20) + layout.setContentsMargins(50, 50, 50, 50) + + title_label = QLabel("Цифровой помощник биохимика") + title_font = QFont() + title_font.setPointSize(20) + title_font.setBold(True) + title_label.setFont(title_font) + title_label.setAlignment(Qt.AlignCenter) + title_label.setStyleSheet("color: #1565C0;") + layout.addWidget(title_label) + + subtitle_label = QLabel("Биотехнологические инструменты для лаборатории") + subtitle_font = QFont() + subtitle_font.setPointSize(12) + subtitle_label.setFont(subtitle_font) + subtitle_label.setAlignment(Qt.AlignCenter) + subtitle_label.setStyleSheet("color: #666;") + layout.addWidget(subtitle_label) + + layout.addSpacing(20) + + btn_medium = QPushButton("🧪 Калькулятор питательных сред") + btn_medium.setMinimumHeight(80) + btn_medium.clicked.connect(self.open_medium_calculator) + layout.addWidget(btn_medium) + + desc1_label = QLabel("Расчёт состава питательной среды с учётом процентного содержания,\nразбавления реагентов и автоматическим расчётом растворителя") + desc1_label.setAlignment(Qt.AlignCenter) + desc1_label.setWordWrap(True) + desc1_label.setStyleSheet("color: #555; font-size: 11px;") + layout.addWidget(desc1_label) + + layout.addSpacing(15) + + btn_experiment = QPushButton("📊 Планирование эксперимента (DoE)") + btn_experiment.setMinimumHeight(80) + btn_experiment.clicked.connect(self.open_experiment_designer) + layout.addWidget(btn_experiment) + + desc2_label = QLabel("Дизайн эксперимента, оптимизация процессов,\nмногомерный анализ и визуализация") + desc2_label.setAlignment(Qt.AlignCenter) + desc2_label.setWordWrap(True) + desc2_label.setStyleSheet("color: #555; font-size: 11px;") + layout.addWidget(desc2_label) + + layout.addSpacing(15) + + line = QFrame() + line.setFrameShape(QFrame.HLine) + line.setFrameShadow(QFrame.Sunken) + layout.addWidget(line) + + bottom_layout = QHBoxLayout() + version_label = QLabel("Версия 1.0.0 | © 2026 Цифровой помощник биохимика") + version_label.setStyleSheet("color: #999; font-size: 10px;") + bottom_layout.addWidget(version_label) + bottom_layout.addStretch() + btn_exit = QPushButton("Выход") + btn_exit.setMaximumWidth(150) + btn_exit.setStyleSheet("QPushButton { background-color: #f44336; padding: 8px; font-size: 14px; } QPushButton:hover { background-color: #da190b; }") + btn_exit.clicked.connect(self.close) + bottom_layout.addWidget(btn_exit) + layout.addLayout(bottom_layout) + + def open_medium_calculator(self): + from .medium_view import MediumCalculatorWindow + from ..controllers.medium_controller import MediumController + self.medium_calculator = MediumCalculatorWindow() + self.medium_controller = MediumController(self.medium_calculator) + self.medium_calculator.show() + + def open_experiment_designer(self): + from .experiment_view import ExperimentDesignWindow + from ..controllers.experiment_controller import ExperimentController + self.experiment_window = ExperimentDesignWindow() + self.experiment_controller = ExperimentController(self.experiment_window) + self.experiment_window.show() +EOF + +# Файл: src/views/medium_view.py +cat > src/views/medium_view.py << 'EOF' +from PyQt5.QtWidgets import (QMainWindow, QVBoxLayout, QHBoxLayout, QTableWidget, QTableWidgetItem, + QPushButton, QLabel, QDoubleSpinBox, QComboBox, QLineEdit, QWidget, + QMessageBox, QGroupBox, QFrame) +from PyQt5.QtCore import Qt +from PyQt5.QtGui import QFont, QColor + +class MediumCalculatorWindow(QMainWindow): + def __init__(self): + super().__init__() + self.setWindowTitle("Калькулятор питательных сред") + self.setGeometry(200, 100, 1200, 700) + self.setStyleSheet(""" + QMainWindow { background-color: #f5f5f5; } + QGroupBox { font-weight: bold; border: 2px solid #ccc; border-radius: 5px; margin-top: 10px; padding-top: 10px; background-color: white; } + QGroupBox::title { subcontrol-origin: margin; left: 10px; padding: 0 5px 0 5px; color: #1565C0; } + QPushButton { background-color: #2196F3; color: white; border: none; padding: 8px 15px; border-radius: 4px; font-weight: bold; } + QPushButton:hover { background-color: #1976D2; } + QPushButton#danger { background-color: #f44336; } + QPushButton#success { background-color: #4CAF50; } + QTableWidget { gridline-color: #ddd; background-color: white; } + QHeaderView::section { background-color: #1565C0; color: white; padding: 8px; font-weight: bold; } + """) + self._init_ui() + + def _init_ui(self): + central_widget = QWidget() + self.setCentralWidget(central_widget) + layout = QVBoxLayout(central_widget) + layout.setSpacing(15) + layout.setContentsMargins(20, 20, 20, 20) + + title_label = QLabel("Калькулятор питательных сред") + title_label.setAlignment(Qt.AlignCenter) + title_font = QFont() + title_font.setPointSize(18) + title_font.setBold(True) + title_label.setFont(title_font) + layout.addWidget(title_label) + + params_group = QGroupBox("Параметры среды") + params_layout = QHBoxLayout() + amount_layout = QHBoxLayout() + amount_layout.addWidget(QLabel("Общее количество:")) + self.amount_input = QDoubleSpinBox() + self.amount_input.setRange(0.001, 1000000.0) + self.amount_input.setValue(1000.0) + amount_layout.addWidget(self.amount_input) + self.amount_unit_combo = QComboBox() + self.amount_unit_combo.addItems(["нл", "мкл", "мл", "л"]) + self.amount_unit_combo.setCurrentText("мл") + amount_layout.addWidget(self.amount_unit_combo) + params_layout.addLayout(amount_layout) + solvent_layout = QHBoxLayout() + solvent_layout.addWidget(QLabel("Растворитель:")) + self.solvent_input = QLineEdit("Вода") + solvent_layout.addWidget(self.solvent_input) + params_layout.addLayout(solvent_layout) + params_layout.addStretch() + params_group.setLayout(params_layout) + layout.addWidget(params_group) + + table_group = QGroupBox("Состав среды") + table_layout = QVBoxLayout() + self.table = QTableWidget() + self.table.setColumnCount(6) + self.table.setHorizontalHeaderLabels(["Название", "%", "Единица", "Коэфф.", "Разбавление (x)", "Количество"]) + self.table.setAlternatingRowColors(True) + self.table.setSelectionBehavior(QTableWidget.SelectRows) + self.table.setColumnWidth(0, 180) + self.table.setColumnWidth(1, 70) + self.table.setColumnWidth(2, 90) + self.table.setColumnWidth(3, 70) + self.table.setColumnWidth(4, 100) + self.table.setColumnWidth(5, 120) + table_layout.addWidget(self.table) + table_group.setLayout(table_layout) + layout.addWidget(table_group) + + btn_group = QGroupBox("Управление") + btn_layout = QHBoxLayout() + self.add_row_btn = QPushButton("➕ Добавить реагент") + self.remove_row_btn = QPushButton("➖ Удалить реагент") + self.remove_row_btn.setObjectName("danger") + btn_layout.addWidget(self.add_row_btn) + btn_layout.addWidget(self.remove_row_btn) + btn_layout.addStretch() + self.calculate_btn = QPushButton("🧮 Рассчитать") + self.calculate_btn.setObjectName("success") + self.save_btn = QPushButton("💾 Сохранить") + self.load_btn = QPushButton("📂 Загрузить") + btn_layout.addWidget(self.calculate_btn) + btn_layout.addWidget(self.save_btn) + btn_layout.addWidget(self.load_btn) + btn_group.setLayout(btn_layout) + layout.addWidget(btn_group) + + info_frame = QFrame() + info_frame.setFrameShape(QFrame.StyledPanel) + info_frame.setStyleSheet("background-color: #e3f2fd; border-radius: 5px;") + info_layout = QHBoxLayout(info_frame) + info_label = QLabel("ℹ️ Подсказка: Реагенты в массовых единицах не учитываются при расчёте объёма растворителя") + info_layout.addWidget(info_label) + info_layout.addStretch() + layout.addWidget(info_frame) + + def add_initial_rows(self): + self.add_solvent_row() + self.add_new_row() + + def add_solvent_row(self): + row = self.table.rowCount() + self.table.insertRow(row) + solvent_item = QTableWidgetItem(self.solvent_input.text()) + solvent_item.setFlags(solvent_item.flags() & ~Qt.ItemIsEditable) + solvent_item.setBackground(QColor(230, 230, 230)) + font = QFont() + font.setBold(True) + solvent_item.setFont(font) + self.table.setItem(row, 0, solvent_item) + for col in [1, 3, 4]: + item = QTableWidgetItem("-") + item.setFlags(item.flags() & ~Qt.ItemIsEditable) + item.setBackground(QColor(230, 230, 230)) + self.table.setItem(row, col, item) + unit_item = QTableWidgetItem(self.amount_unit_combo.currentText()) + unit_item.setFlags(unit_item.flags() & ~Qt.ItemIsEditable) + unit_item.setBackground(QColor(230, 230, 230)) + self.table.setItem(row, 2, unit_item) + result_item = QTableWidgetItem("") + result_item.setFlags(result_item.flags() & ~Qt.ItemIsEditable) + self.table.setItem(row, 5, result_item) + + def update_solvent_name(self): + if self.table.rowCount() > 0 and self.table.item(0, 0): + self.table.item(0, 0).setText(self.solvent_input.text()) + + def add_new_row(self): + row = self.table.rowCount() + self.table.insertRow(row) + self.table.setItem(row, 0, QTableWidgetItem(f"Реагент_{row}")) + self.table.setItem(row, 1, QTableWidgetItem("0")) + unit_combo = QComboBox() + unit_combo.addItems(["нг", "мкг", "мг", "г", "кг", "нл", "мкл", "мл", "л"]) + unit_combo.setCurrentText("мг") + self.table.setCellWidget(row, 2, unit_combo) + self.table.setItem(row, 3, QTableWidgetItem("1")) + self.table.setItem(row, 4, QTableWidgetItem("1")) + self.table.setItem(row, 5, QTableWidgetItem("")) + + def remove_selected_row(self): + for row in sorted(set(item.row() for item in self.table.selectedItems()), reverse=True): + if row > 0: + self.table.removeRow(row) + + def get_table_data(self): + data = [] + for row in range(1, self.table.rowCount()): + name = self.table.item(row, 0).text() if self.table.item(row, 0) else "" + percent = self.table.item(row, 1).text() if self.table.item(row, 1) else "0" + unit_widget = self.table.cellWidget(row, 2) + unit = unit_widget.currentText() if unit_widget else "мг" + coeff = self.table.item(row, 3).text() if self.table.item(row, 3) else "1" + dilution = self.table.item(row, 4).text() if self.table.item(row, 4) else "1" + data.append([name, percent, unit, coeff, dilution]) + return data + + def update_solvent_percent(self, solvent_percent: float): + if self.table.rowCount() > 0 and self.table.item(0, 1): + self.table.item(0, 1).setText(self.format_number(solvent_percent)) + + def update_solvent_result(self, solvent_amount: float, unit: str): + if self.table.rowCount() > 0: + if self.table.item(0, 5): + self.table.item(0, 5).setText(self.format_number(solvent_amount)) + self.table.item(0, 5).setBackground(QColor(220, 255, 220)) + if self.table.item(0, 2): + self.table.item(0, 2).setText(unit) + + def update_results(self, results: list): + for row, amount in enumerate(results, start=1): + if row < self.table.rowCount() and self.table.item(row, 5): + self.table.item(row, 5).setText(self.format_number(amount)) + self.table.item(row, 5).setBackground(QColor(220, 255, 220)) + + def clear_results(self): + for row in range(self.table.rowCount()): + if self.table.item(row, 5): + self.table.item(row, 5).setText("") + if row == 0: + self.table.item(row, 5).setBackground(QColor(230, 230, 230)) + else: + self.table.item(row, 5).setBackground(QColor(250, 250, 250)) + if self.table.rowCount() > 0 and self.table.item(0, 1): + self.table.item(0, 1).setText("") + if self.table.rowCount() > 0 and self.table.item(0, 2): + self.table.item(0, 2).setText(self.amount_unit_combo.currentText()) + + def format_number(self, value): + if value == int(value): + return str(int(value)) + formatted = f"{value:.6f}".rstrip('0').rstrip('.') + if '.' in formatted and len(formatted.split('.')[1]) > 4: + formatted = f"{value:.4f}".rstrip('0').rstrip('.') + return formatted + + def show_error(self, message: str): + QMessageBox.critical(self, "Ошибка", message) +EOF + +# Файл: src/views/experiment_view.py (полная версия) +cat > src/views/experiment_view.py << 'EOF' +from PyQt5.QtWidgets import (QMainWindow, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, + QWidget, QMessageBox, QTableWidget, QTableWidgetItem, QGroupBox, + QSpinBox, QDoubleSpinBox, QComboBox, QLineEdit, QTextEdit, + QTabWidget, QFormLayout, QCheckBox, QScrollArea, QFileDialog) +from PyQt5.QtCore import Qt +from PyQt5.QtGui import QColor, QFont +import csv +import random +import numpy as np + +class ExperimentDesignWindow(QMainWindow): + def __init__(self): + super().__init__() + self.setWindowTitle("Планирование эксперимента - Цифровой помощник биохимика") + self.setGeometry(200, 100, 1200, 800) + self.setStyleSheet(""" + QMainWindow { background-color: #f5f5f5; } + QGroupBox { font-weight: bold; border: 2px solid #ccc; border-radius: 5px; margin-top: 10px; padding-top: 10px; } + QGroupBox::title { subcontrol-origin: margin; left: 10px; padding: 0 5px 0 5px; color: #1565C0; } + QPushButton { background-color: #4CAF50; color: white; border: none; padding: 8px; border-radius: 4px; font-weight: bold; } + QPushButton:hover { background-color: #45a049; } + QPushButton#danger { background-color: #f44336; } + QPushButton#danger:hover { background-color: #da190b; } + QTableWidget { gridline-color: #ddd; } + QHeaderView::section { background-color: #1565C0; color: white; padding: 8px; font-weight: bold; } + """) + self._init_ui() + + def _init_ui(self): + central_widget = QWidget() + self.setCentralWidget(central_widget) + layout = QVBoxLayout(central_widget) + + title_label = QLabel("Планирование полнофакторного эксперимента (DoE)") + title_font = QFont() + title_font.setPointSize(18) + title_font.setBold(True) + title_label.setFont(title_font) + title_label.setAlignment(Qt.AlignCenter) + title_label.setStyleSheet("color: #2E7D32;") + layout.addWidget(title_label) + + tabs = QTabWidget() + + # Вкладка параметров + params_tab = QWidget() + params_layout = QVBoxLayout(params_tab) + + factors_group = QGroupBox("Факторы эксперимента (независимые переменные)") + factors_layout = QVBoxLayout() + + info_label = QLabel("Определите факторы, которые влияют на ваш эксперимент:") + info_label.setStyleSheet("color: #555; font-weight: normal;") + factors_layout.addWidget(info_label) + + self.factors_table = QTableWidget() + self.factors_table.setColumnCount(6) + self.factors_table.setHorizontalHeaderLabels(["Фактор", "Нулевой уровень (0)", "Шаг", + "Верхний уровень (+1)", "Нижний уровень (-1)", "Единица измерения"]) + self.factors_table.setRowCount(2) + + sample_factors = [["Температура", "31", "6", "37", "25", "°C"], ["pH", "7.0", "0.5", "7.5", "6.5", ""]] + for i, factor in enumerate(sample_factors): + for j, value in enumerate(factor): + item = QTableWidgetItem(value) + if j in [3, 4]: + item.setFlags(item.flags() & ~Qt.ItemIsEditable) + item.setBackground(QColor(240, 240, 240)) + self.factors_table.setItem(i, j, item) + + self.factors_table.setColumnWidth(0, 150) + self.factors_table.setColumnWidth(1, 120) + self.factors_table.setColumnWidth(2, 80) + self.factors_table.setColumnWidth(3, 120) + self.factors_table.setColumnWidth(4, 120) + self.factors_table.setColumnWidth(5, 100) + + self.factors_table.itemChanged.connect(self.on_factor_changed) + factors_layout.addWidget(self.factors_table) + + factor_buttons = QHBoxLayout() + add_factor_btn = QPushButton("+ Добавить фактор") + add_factor_btn.clicked.connect(self.add_factor_row) + remove_factor_btn = QPushButton("- Удалить последний") + remove_factor_btn.clicked.connect(self.remove_factor_row) + factor_buttons.addWidget(add_factor_btn) + factor_buttons.addWidget(remove_factor_btn) + factor_buttons.addStretch() + factors_layout.addLayout(factor_buttons) + factors_group.setLayout(factors_layout) + params_layout.addWidget(factors_group) + + settings_group = QGroupBox("Настройки эксперимента") + settings_layout = QHBoxLayout() + center_layout = QHBoxLayout() + center_layout.addWidget(QLabel("Количество центральных точек:")) + self.center_points_spin = QSpinBox() + self.center_points_spin.setRange(0, 10) + self.center_points_spin.setValue(3) + center_layout.addWidget(self.center_points_spin) + settings_layout.addLayout(center_layout) + self.randomize_check = QCheckBox("Рэндомизировать порядок опытов") + self.randomize_check.setChecked(True) + settings_layout.addWidget(self.randomize_check) + settings_layout.addStretch() + settings_group.setLayout(settings_layout) + params_layout.addWidget(settings_group) + + responses_group = QGroupBox("Отклики (зависимые переменные)") + responses_layout = QVBoxLayout() + self.responses_table = QTableWidget() + self.responses_table.setColumnCount(2) + self.responses_table.setHorizontalHeaderLabels(["Отклик", "Единица измерения"]) + self.responses_table.setRowCount(2) + sample_responses = [["Оптическая плотность (OD600)", ""], ["Концентрация целевого продукта", "мг/мл"]] + for i, response in enumerate(sample_responses): + for j, value in enumerate(response): + self.responses_table.setItem(i, j, QTableWidgetItem(value)) + responses_layout.addWidget(self.responses_table) + + response_buttons = QHBoxLayout() + add_response_btn = QPushButton("+ Добавить отклик") + add_response_btn.clicked.connect(self.add_response_row) + remove_response_btn = QPushButton("- Удалить последний") + remove_response_btn.clicked.connect(self.remove_response_row) + response_buttons.addWidget(add_response_btn) + response_buttons.addWidget(remove_response_btn) + response_buttons.addStretch() + responses_layout.addLayout(response_buttons) + responses_group.setLayout(responses_layout) + params_layout.addWidget(responses_group) + tabs.addTab(params_tab, "📝 Параметры эксперимента") + + # Вкладка матрицы планирования + plan_tab = QWidget() + plan_layout = QVBoxLayout(plan_tab) + plan_info = QLabel("Полнофакторный план эксперимента с центральными точками") + plan_info.setAlignment(Qt.AlignCenter) + plan_info.setStyleSheet("font-weight: bold; font-size: 14px; padding: 10px;") + plan_layout.addWidget(plan_info) + + scroll = QScrollArea() + scroll.setWidgetResizable(True) + matrix_widget = QWidget() + matrix_layout = QVBoxLayout(matrix_widget) + self.design_matrix = QTableWidget() + matrix_layout.addWidget(self.design_matrix) + scroll.setWidget(matrix_widget) + plan_layout.addWidget(scroll) + + buttons_layout = QHBoxLayout() + generate_btn = QPushButton("Сгенерировать план эксперимента") + generate_btn.clicked.connect(self.generate_design_matrix) + buttons_layout.addWidget(generate_btn) + export_btn = QPushButton("📊 Экспорт в CSV") + export_btn.clicked.connect(self.export_to_csv) + buttons_layout.addWidget(export_btn) + buttons_layout.addStretch() + plan_layout.addLayout(buttons_layout) + + self.plan_info_label = QLabel("") + self.plan_info_label.setStyleSheet("color: #666; font-size: 12px; padding: 5px;") + plan_layout.addWidget(self.plan_info_label) + tabs.addTab(plan_tab, "📊 Матрица планирования") + + # Вкладка анализа + analysis_tab = QWidget() + analysis_layout = QVBoxLayout(analysis_tab) + analysis_info = QLabel("Введите результаты экспериментов для анализа") + analysis_info.setAlignment(Qt.AlignCenter) + analysis_info.setStyleSheet("font-weight: bold; font-size: 14px; padding: 10px;") + analysis_layout.addWidget(analysis_info) + self.results_table = QTableWidget() + analysis_layout.addWidget(self.results_table) + analyze_btn = QPushButton("Провести регрессионный анализ") + analyze_btn.clicked.connect(self.perform_analysis) + analysis_layout.addWidget(analyze_btn) + self.analysis_output = QTextEdit() + self.analysis_output.setReadOnly(True) + self.analysis_output.setMaximumHeight(200) + analysis_layout.addWidget(self.analysis_output) + tabs.addTab(analysis_tab, "📈 Анализ результатов") + + layout.addWidget(tabs) + + btn_layout = QHBoxLayout() + close_btn = QPushButton("Закрыть") + close_btn.clicked.connect(self.close) + btn_layout.addStretch() + btn_layout.addWidget(close_btn) + layout.addLayout(btn_layout) + + self.generated_design = None + self.factors_data = None + + def on_factor_changed(self, item): + row = item.row() + col = item.column() + if col in [1, 2]: + try: + center = float(self.factors_table.item(row, 1).text()) if self.factors_table.item(row, 1) else 0 + step = float(self.factors_table.item(row, 2).text()) if self.factors_table.item(row, 2) else 0 + high = center + step + low = center - step + self.factors_table.blockSignals(True) + if self.factors_table.item(row, 3): + self.factors_table.item(row, 3).setText(f"{high:.3f}".rstrip('0').rstrip('.')) + if self.factors_table.item(row, 4): + self.factors_table.item(row, 4).setText(f"{low:.3f}".rstrip('0').rstrip('.')) + self.factors_table.blockSignals(False) + except ValueError: + pass + + def add_factor_row(self): + row = self.factors_table.rowCount() + self.factors_table.insertRow(row) + self.factors_table.setItem(row, 0, QTableWidgetItem(f"Фактор_{row+1}")) + self.factors_table.setItem(row, 1, QTableWidgetItem("0")) + self.factors_table.setItem(row, 2, QTableWidgetItem("1")) + high_item = QTableWidgetItem("1") + high_item.setFlags(high_item.flags() & ~Qt.ItemIsEditable) + high_item.setBackground(QColor(240, 240, 240)) + self.factors_table.setItem(row, 3, high_item) + low_item = QTableWidgetItem("-1") + low_item.setFlags(low_item.flags() & ~Qt.ItemIsEditable) + low_item.setBackground(QColor(240, 240, 240)) + self.factors_table.setItem(row, 4, low_item) + self.factors_table.setItem(row, 5, QTableWidgetItem("")) + + def remove_factor_row(self): + if self.factors_table.rowCount() > 1: + self.factors_table.removeRow(self.factors_table.rowCount() - 1) + + def add_response_row(self): + row = self.responses_table.rowCount() + self.responses_table.insertRow(row) + self.responses_table.setItem(row, 0, QTableWidgetItem(f"Отклик_{row+1}")) + self.responses_table.setItem(row, 1, QTableWidgetItem("")) + + def remove_response_row(self): + if self.responses_table.rowCount() > 1: + self.responses_table.removeRow(self.responses_table.rowCount() - 1) + + def get_factors_data(self): + factors = [] + for row in range(self.factors_table.rowCount()): + try: + factor = { + 'name': self.factors_table.item(row, 0).text() if self.factors_table.item(row, 0) else "", + 'center': float(self.factors_table.item(row, 1).text()) if self.factors_table.item(row, 1) else 0, + 'step': float(self.factors_table.item(row, 2).text()) if self.factors_table.item(row, 2) else 0, + 'high': float(self.factors_table.item(row, 3).text()) if self.factors_table.item(row, 3) else 0, + 'low': float(self.factors_table.item(row, 4).text()) if self.factors_table.item(row, 4) else 0, + 'unit': self.factors_table.item(row, 5).text() if self.factors_table.item(row, 5) else "" + } + factors.append(factor) + except (ValueError, AttributeError): + continue + return factors + + def calculate_factorial_design(self, factors): + k = len(factors) + if k == 0: + return [] + n_factorial = 2 ** k + design = [] + for i in range(n_factorial): + experiment = {} + for j in range(k): + coded_level = -1 if (i >> (k - 1 - j)) & 1 == 0 else 1 + natural_value = factors[j]['low'] if coded_level == -1 else factors[j]['high'] + experiment[f"Фактор_{j+1}"] = { + 'coded': coded_level, + 'natural': natural_value, + 'name': factors[j]['name'], + 'unit': factors[j]['unit'] + } + design.append(experiment) + + n_center = self.center_points_spin.value() + for i in range(n_center): + center_experiment = {} + for j in range(k): + center_experiment[f"Фактор_{j+1}"] = { + 'coded': 0, + 'natural': factors[j]['center'], + 'name': factors[j]['name'], + 'unit': factors[j]['unit'] + } + center_experiment['is_center'] = True + center_experiment['center_num'] = i + 1 + design.append(center_experiment) + + if self.randomize_check.isChecked(): + random.shuffle(design) + return design + + def generate_design_matrix(self): + factors = self.get_factors_data() + if len(factors) == 0: + QMessageBox.warning(self, "Предупреждение", "Добавьте хотя бы один фактор!") + return + + self.factors_data = factors + design = self.calculate_factorial_design(factors) + self.generated_design = design + + n_experiments = len(design) + n_factors = len(factors) + + self.design_matrix.setRowCount(n_experiments) + self.design_matrix.setColumnCount(n_factors + 2) + headers = ["№ опыта"] + [f["name"] for f in factors] + ["Тип точки"] + self.design_matrix.setHorizontalHeaderLabels(headers) + + for exp_idx, experiment in enumerate(design): + self.design_matrix.setItem(exp_idx, 0, QTableWidgetItem(str(exp_idx + 1))) + for factor_idx in range(n_factors): + factor_key = f"Фактор_{factor_idx + 1}" + value = experiment[factor_key]['natural'] + unit = factors[factor_idx]['unit'] + if isinstance(value, float): + if value == int(value): + display_value = str(int(value)) + else: + display_value = f"{value:.3f}".rstrip('0').rstrip('.') + else: + display_value = str(value) + if unit: + display_value += f" {unit}" + item = QTableWidgetItem(display_value) + if experiment.get('is_center', False): + item.setBackground(QColor(255, 255, 200)) + self.design_matrix.setItem(exp_idx, factor_idx + 1, item) + + if experiment.get('is_center', False): + type_item = QTableWidgetItem(f"Центральная #{experiment['center_num']}") + type_item.setBackground(QColor(255, 255, 200)) + else: + levels = [] + for factor_idx in range(n_factors): + factor_key = f"Фактор_{factor_idx + 1}" + coded = experiment[factor_key]['coded'] + levels.append("+" if coded == 1 else "-") + type_item = QTableWidgetItem(f"Факторная ({''.join(levels)})") + self.design_matrix.setItem(exp_idx, n_factors + 1, type_item) + + self.design_matrix.resizeColumnsToContents() + n_factorial = 2 ** n_factors + n_center = self.center_points_spin.value() + self.plan_info_label.setText(f"📊 План эксперимента: {n_factorial} факторных точек + {n_center} центральных точек = {n_experiments} опытов") + self.setup_results_table(n_experiments) + QMessageBox.information(self, "Успех", f"Сгенерирован план для {n_factors} факторов\nФакторных точек: {n_factorial}\nЦентральных точек: {n_center}\nВсего опытов: {n_experiments}") + + def setup_results_table(self, n_experiments): + n_responses = self.responses_table.rowCount() + self.results_table.setRowCount(n_experiments) + self.results_table.setColumnCount(n_responses + 1) + headers = ["№ опыта"] + [self.responses_table.item(i, 0).text() if self.responses_table.item(i, 0) else f"Отклик_{i+1}" for i in range(n_responses)] + self.results_table.setHorizontalHeaderLabels(headers) + for i in range(n_experiments): + self.results_table.setItem(i, 0, QTableWidgetItem(str(i + 1))) + self.results_table.setColumnWidth(0, 80) + for i in range(n_responses): + self.results_table.setColumnWidth(i + 1, 150) + + def export_to_csv(self): + if self.design_matrix.rowCount() == 0: + QMessageBox.warning(self, "Предупреждение", "Сначала сгенерируйте план эксперимента!") + return + filename, _ = QFileDialog.getSaveFileName(self, "Сохранить план эксперимента", "", "CSV Files (*.csv);;All Files (*)") + if filename: + if not filename.lower().endswith('.csv'): + filename += '.csv' + try: + with open(filename, 'w', newline='', encoding='utf-8-sig') as f: + writer = csv.writer(f) + headers = [] + for j in range(self.design_matrix.columnCount()): + header_item = self.design_matrix.horizontalHeaderItem(j) + headers.append(header_item.text() if header_item else f"Колонка_{j+1}") + writer.writerow(headers) + for i in range(self.design_matrix.rowCount()): + row = [self.design_matrix.item(i, j).text() if self.design_matrix.item(i, j) else "" for j in range(self.design_matrix.columnCount())] + writer.writerow(row) + QMessageBox.information(self, "Успех", f"План эксперимента сохранен в {filename}") + except Exception as e: + QMessageBox.critical(self, "Ошибка", f"Не удалось сохранить файл: {str(e)}") + + def perform_analysis(self): + n_responses = self.responses_table.rowCount() + if n_responses == 0: + QMessageBox.warning(self, "Предупреждение", "Добавьте хотя бы один отклик!") + return + if self.generated_design is None: + QMessageBox.warning(self, "Предупреждение", "Сначала сгенерируйте план эксперимента!") + return + + results = [] + for i in range(self.results_table.rowCount()): + row_results = [] + for j in range(1, self.results_table.columnCount()): + item = self.results_table.item(i, j) + if item and item.text(): + try: + row_results.append(float(item.text())) + except ValueError: + row_results.append(None) + else: + row_results.append(None) + results.append(row_results) + + for i, row in enumerate(results): + for j, val in enumerate(row): + if val is None: + self.analysis_output.setText(f"Ошибка: Не введены результаты для опыта {i+1}, отклик {j+1}") + return + + self.analysis_output.clear() + self.analysis_output.append("=" * 60) + self.analysis_output.append("РЕЗУЛЬТАТЫ РЕГРЕССИОННОГО АНАЛИЗА") + self.analysis_output.append("=" * 60) + + factors = self.get_factors_data() + design = self.generated_design + + for resp_idx in range(n_responses): + resp_name = self.responses_table.item(resp_idx, 0).text() + self.analysis_output.append(f"\n📊 Отклик: {resp_name}") + self.analysis_output.append("-" * 40) + + y_values = [results[i][resp_idx] for i in range(len(results))] + mean_y = np.mean(y_values) + variance = np.var(y_values, ddof=1) if len(y_values) > 1 else 0 + std_dev = np.std(y_values, ddof=1) if len(y_values) > 1 else 0 + cv = (std_dev / mean_y) * 100 if mean_y != 0 else 0 + + self.analysis_output.append(f"Среднее значение: {mean_y:.4f}") + self.analysis_output.append(f"Общая дисперсия: {variance:.4f}") + self.analysis_output.append(f"Стандартное отклонение: {std_dev:.4f}") + self.analysis_output.append(f"Коэффициент вариации: {cv:.2f}%") + + factorial_y = [] + center_y = [] + for i, exp in enumerate(design): + if exp.get('is_center', False): + center_y.append(y_values[i]) + else: + factorial_y.append(y_values[i]) + + if len(center_y) > 1: + center_variance = np.var(center_y, ddof=1) if len(center_y) > 1 else 0 + self.analysis_output.append(f"\nЦентральные точки (n={len(center_y)}):") + self.analysis_output.append(f" Среднее: {np.mean(center_y):.4f}") + self.analysis_output.append(f" Дисперсия воспроизводимости: {center_variance:.4f}") + if len(factorial_y) > 0 and center_variance > 0: + factorial_variance = np.var(factorial_y, ddof=1) if len(factorial_y) > 1 else 0 + if factorial_variance > 0: + fisher = max(factorial_variance, center_variance) / min(factorial_variance, center_variance) + self.analysis_output.append(f"\nКритерий Фишера (F-отношение): {fisher:.4f}") + if fisher < 4.0: + self.analysis_output.append("✅ Модель адекватна экспериментальным данным") + else: + self.analysis_output.append("⚠️ Модель может быть неадекватна, требуется проверка") + + self.analysis_output.append("\n" + "=" * 60) + self.analysis_output.append("Анализ завершен") + + def show_error(self, message: str): + QMessageBox.critical(self, "Ошибка", message) +EOF + +# Файл: src/controllers/__init__.py +cat > src/controllers/__init__.py << 'EOF' +from .medium_controller import MediumController +from .experiment_controller import ExperimentController + +__all__ = ['MediumController', 'ExperimentController'] +EOF + +# Файл: src/controllers/medium_controller.py +cat > src/controllers/medium_controller.py << 'EOF' +from PyQt5.QtWidgets import QFileDialog, QMessageBox, QTableWidgetItem, QComboBox +from PyQt5.QtCore import Qt +from PyQt5.QtGui import QColor +import json +from ..models.medium_model import MediumModel +from ..models.reagent import Reagent + +class MediumController: + def __init__(self, view): + self.model = MediumModel() + self.view = view + self._connect_signals() + self._setup_initial_data() + + def _connect_signals(self): + self.view.add_row_btn.clicked.connect(self.add_reagent_row) + self.view.remove_row_btn.clicked.connect(self.remove_reagent_row) + self.view.calculate_btn.clicked.connect(self._perform_calculation) + self.view.save_btn.clicked.connect(self.save_composition) + self.view.load_btn.clicked.connect(self.load_composition) + self.view.solvent_input.textChanged.connect(self.view.update_solvent_name) + + def _setup_initial_data(self): + self.view.add_initial_rows() + + def add_reagent_row(self): + self.view.add_new_row() + + def remove_reagent_row(self): + self.view.remove_selected_row() + + def _perform_calculation(self): + try: + self._update_model_from_view() + results, solvent_amount, solvent_percentage = self.model.calculate_amounts() + self.view.update_results(results) + self.view.update_solvent_result(solvent_amount, self.model.amount_unit) + self.view.update_solvent_percent(solvent_percentage) + except ValueError as e: + self.view.show_error(f"Ошибка в данных: {str(e)}") + except Exception as e: + self.view.show_error(f"Неожиданная ошибка: {str(e)}") + + def _update_model_from_view(self): + self.model.reagents.clear() + self.model.total_amount = self.view.amount_input.value() + self.model.amount_unit = self.view.amount_unit_combo.currentText() + self.model.solvent = self.view.solvent_input.text() + for row in range(1, self.view.table.rowCount()): + name_item = self.view.table.item(row, 0) + percentage_item = self.view.table.item(row, 1) + unit_widget = self.view.table.cellWidget(row, 2) + conversion_item = self.view.table.item(row, 3) + dilution_item = self.view.table.item(row, 4) + if not all([name_item, percentage_item, conversion_item]): + continue + try: + name = name_item.text() + percentage = float(percentage_item.text()) + unit = unit_widget.currentText() if unit_widget else "мг" + conversion_factor = float(conversion_item.text()) + dilution_factor = float(dilution_item.text()) if dilution_item else 1.0 + reagent = Reagent(name, percentage, unit, conversion_factor) + reagent.dilution_factor = dilution_factor + self.model.reagents.append(reagent) + except ValueError as e: + raise ValueError(f"Ошибка в строке {row + 1}: {str(e)}") + + def save_composition(self): + filename, _ = QFileDialog.getSaveFileName(self.view, "Сохранить состав среды", "", "JSON Files (*.json);;All Files (*)") + if filename: + if not filename.lower().endswith('.json'): + filename += '.json' + try: + self._update_model_from_view() + self.model.save_to_file(filename) + QMessageBox.information(self.view, "Успех", "Состав среды успешно сохранён!") + except Exception as e: + self.view.show_error(f"Ошибка сохранения: {str(e)}") + + def load_composition(self): + filename, _ = QFileDialog.getOpenFileName(self.view, "Загрузить состав среды", "", "JSON Files (*.json);;All Files (*)") + if filename: + try: + self.model.load_from_file(filename) + self._update_view_from_model() + QMessageBox.information(self.view, "Успех", "Состав среды успешно загружен") + except FileNotFoundError: + QMessageBox.critical(self.view, "Ошибка", f"Файл не найден: {filename}") + except json.JSONDecodeError as e: + QMessageBox.critical(self.view, "Ошибка", f"Неверный формат JSON-файла: {str(e)}") + except Exception as e: + QMessageBox.critical(self.view, "Ошибка", f"Ошибка при загрузке состава: {str(e)}") + + def _update_view_from_model(self): + while self.view.table.rowCount() > 1: + self.view.table.removeRow(1) + if self.view.table.rowCount() == 0: + self.view.add_solvent_row() + self.view.amount_input.setValue(self.model.total_amount) + index = self.view.amount_unit_combo.findText(self.model.amount_unit) + if index >= 0: + self.view.amount_unit_combo.setCurrentIndex(index) + self.view.solvent_input.setText(self.model.solvent) + self.view.update_solvent_name() + for reagent in self.model.reagents: + row = self.view.table.rowCount() + self.view.table.insertRow(row) + self.view.table.setItem(row, 0, QTableWidgetItem(reagent.name)) + self.view.table.setItem(row, 1, QTableWidgetItem(f"{reagent.percentage:.2f}")) + unit_combo = QComboBox() + unit_combo.addItems(["нг", "мкг", "мг", "г", "кг", "нл", "мкл", "мл", "л"]) + unit_combo.setCurrentText(reagent.unit) + self.view.table.setCellWidget(row, 2, unit_combo) + self.view.table.setItem(row, 3, QTableWidgetItem(f"{reagent.conversion_factor:.2f}")) + self.view.table.setItem(row, 4, QTableWidgetItem(f"{getattr(reagent, 'dilution_factor', 1.0):.3f}")) + self.view.table.setItem(row, 5, QTableWidgetItem("")) + self.view.clear_results() +EOF + +# Файл: src/controllers/experiment_controller.py +cat > src/controllers/experiment_controller.py << 'EOF' +from ..models.experiment_model import ExperimentModel + +class ExperimentController: + def __init__(self, view): + self.model = ExperimentModel() + self.view = view + + def update_model_from_view(self): + factors = self.view.get_factors_data() + self.model.set_factors(factors) + + responses = [] + for row in range(self.view.responses_table.rowCount()): + name_item = self.view.responses_table.item(row, 0) + unit_item = self.view.responses_table.item(row, 1) + if name_item: + responses.append({ + 'name': name_item.text(), + 'unit': unit_item.text() if unit_item else "" + }) + self.model.set_responses(responses) + self.model.set_center_points(self.view.center_points_spin.value()) + self.model.set_randomize(self.view.randomize_check.isChecked()) +EOF + +# Файл: main.py +cat > main.py << 'EOF' +import sys +import os +sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src')) + +from PyQt5.QtWidgets import QApplication +from src.views import MainWindow + +def main(): + app = QApplication(sys.argv) + app.setStyle('Fusion') + assistant = MainWindow() + assistant.show() + sys.exit(app.exec_()) + +if __name__ == "__main__": + main() +EOF + +# Файл: run.sh +cat > run.sh << 'EOF' +#!/bin/bash +python3 main.py +EOF +chmod +x run.sh + +# Файл: requirements.txt +cat > requirements.txt << 'EOF' +PyQt5>=5.15.0 +numpy>=1.19.0 +EOF + +echo -e "${GREEN}========================================${NC}" +echo -e "${GREEN}Реорганизация завершена!${NC}" +echo -e "${GREEN}========================================${NC}" +echo -e "${YELLOW}Для запуска обновлённой версии:${NC}" +echo -e "${GREEN}python3 main.py${NC}" +echo "" +echo -e "${GREEN}или${NC}" +echo -e "${GREEN}./run.sh${NC}" +echo "" +echo -e "${YELLOW}Бэкап старых файлов:${NC} $BACKUP_DIR" diff --git a/reagent.py b/reagent.py deleted file mode 100644 index cd8a384..0000000 --- a/reagent.py +++ /dev/null @@ -1,7 +0,0 @@ -class Reagent: - def __init__(self, name: str, percentage: float, unit: str, conversion_factor: float = 1.0): - self.name = name - self.percentage = percentage - self.unit = unit - self.conversion_factor = conversion_factor # коэффициент пересчёта - self.dilution_factor = 1.0 # во сколько раз разбавляем (1 = без разбавления) diff --git a/requirements.txt b/requirements.txt index 93b18bc..b7803ce 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ PyQt5>=5.15.0 +numpy>=1.19.0 diff --git a/run.sh b/run.sh new file mode 100755 index 0000000..e1f6228 --- /dev/null +++ b/run.sh @@ -0,0 +1,2 @@ +#!/bin/bash +python3 main.py diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..e878fc2 --- /dev/null +++ b/src/__init__.py @@ -0,0 +1,3 @@ +"""Цифровой помощник биохимика - основная библиотека""" + +__version__ = "1.0.0" diff --git a/src/controllers/__init__.py b/src/controllers/__init__.py new file mode 100644 index 0000000..37f75f8 --- /dev/null +++ b/src/controllers/__init__.py @@ -0,0 +1,4 @@ +from .medium_controller import MediumController +from .experiment_controller import ExperimentController + +__all__ = ['MediumController', 'ExperimentController'] diff --git a/src/controllers/experiment_controller.py b/src/controllers/experiment_controller.py new file mode 100644 index 0000000..023e7fa --- /dev/null +++ b/src/controllers/experiment_controller.py @@ -0,0 +1,23 @@ +from ..models.experiment_model import ExperimentModel + +class ExperimentController: + def __init__(self, view): + self.model = ExperimentModel() + self.view = view + + def update_model_from_view(self): + factors = self.view.get_factors_data() + self.model.set_factors(factors) + + responses = [] + for row in range(self.view.responses_table.rowCount()): + name_item = self.view.responses_table.item(row, 0) + unit_item = self.view.responses_table.item(row, 1) + if name_item: + responses.append({ + 'name': name_item.text(), + 'unit': unit_item.text() if unit_item else "" + }) + self.model.set_responses(responses) + self.model.set_center_points(self.view.center_points_spin.value()) + self.model.set_randomize(self.view.randomize_check.isChecked()) diff --git a/controller.py b/src/controllers/medium_controller.py similarity index 55% rename from controller.py rename to src/controllers/medium_controller.py index cd0bdfb..6b4ff94 100644 --- a/controller.py +++ b/src/controllers/medium_controller.py @@ -1,182 +1,118 @@ -from PyQt5.QtWidgets import QMessageBox, QFileDialog, QTableWidgetItem, QComboBox, QLineEdit, QDoubleSpinBox +from PyQt5.QtWidgets import QFileDialog, QMessageBox, QTableWidgetItem, QComboBox from PyQt5.QtCore import Qt from PyQt5.QtGui import QColor -from model import Model -from view import MediumCalculatorWindow import json -from reagent import Reagent +from ..models.medium_model import MediumModel +from ..models.reagent import Reagent - -class Controller: - def __init__(self): - self.model = Model() - self.view = MediumCalculatorWindow() +class MediumController: + def __init__(self, view): + self.model = MediumModel() + self.view = view self._connect_signals() - - + self._setup_initial_data() + def _connect_signals(self): - """Подключает обработчики событий интерфейса""" self.view.add_row_btn.clicked.connect(self.add_reagent_row) self.view.remove_row_btn.clicked.connect(self.remove_reagent_row) self.view.calculate_btn.clicked.connect(self._perform_calculation) self.view.save_btn.clicked.connect(self.save_composition) self.view.load_btn.clicked.connect(self.load_composition) self.view.solvent_input.textChanged.connect(self.view.update_solvent_name) - + + def _setup_initial_data(self): + self.view.add_initial_rows() + def add_reagent_row(self): - """Добавляет новую строку для реагента""" self.view.add_new_row() - + def remove_reagent_row(self): - """Удаляет выбранную строку реагента""" self.view.remove_selected_row() - + def _perform_calculation(self): - """Выполняет расчёт и обновляет интерфейс""" try: self._update_model_from_view() results, solvent_amount, solvent_percentage = self.model.calculate_amounts() self.view.update_results(results) - - # Обновляем информацию о растворителе в первой строке таблицы self.view.update_solvent_result(solvent_amount, self.model.amount_unit) self.view.update_solvent_percent(solvent_percentage) - except ValueError as e: self.view.show_error(f"Ошибка в данных: {str(e)}") except Exception as e: self.view.show_error(f"Неожиданная ошибка: {str(e)}") - + def _update_model_from_view(self): - """Обновляет модель данными из интерфейса (только реагенты, без растворителя)""" - # Очищаем список реагентов в модели self.model.reagents.clear() - - # Обновляем общее количество и единицу измерения self.model.total_amount = self.view.amount_input.value() self.model.amount_unit = self.view.amount_unit_combo.currentText() self.model.solvent = self.view.solvent_input.text() - - # Заполняем реагенты из таблицы (начиная с 1 строки, пропуская растворитель) for row in range(1, self.view.table.rowCount()): name_item = self.view.table.item(row, 0) percentage_item = self.view.table.item(row, 1) unit_widget = self.view.table.cellWidget(row, 2) conversion_item = self.view.table.item(row, 3) dilution_item = self.view.table.item(row, 4) - - - # Пропускаем строку, если какие-то обязательные поля отсутствуют if not all([name_item, percentage_item, conversion_item]): continue - try: name = name_item.text() percentage = float(percentage_item.text()) unit = unit_widget.currentText() if unit_widget else "мг" conversion_factor = float(conversion_item.text()) - dilution_factor = float(dilution_item.text()) - - self.model.add_reagent(name, percentage, unit, conversion_factor, dilution_factor) + dilution_factor = float(dilution_item.text()) if dilution_item else 1.0 + reagent = Reagent(name, percentage, unit, conversion_factor) + reagent.dilution_factor = dilution_factor + self.model.reagents.append(reagent) except ValueError as e: raise ValueError(f"Ошибка в строке {row + 1}: {str(e)}") - - def _update_view_from_model(self): - """Обновляет интерфейс данными из модели""" - # Очищаем таблицу, но сохраняем строку растворителя - while self.view.table.rowCount() > 1: - self.view.table.removeRow(1) - - # Если нет строки растворителя, добавляем её - if self.view.table.rowCount() == 0: - self.view.add_solvent_row() - - # Устанавливаем общее количество и единицу измерения - self.view.amount_input.setValue(self.model.total_amount) - index = self.view.amount_unit_combo.findText(self.model.amount_unit) - if index >= 0: - self.view.amount_unit_combo.setCurrentIndex(index) - - # Устанавливаем название растворителя и обновляем его в таблице - self.view.solvent_input.setText(self.model.solvent) - self.view.update_solvent_name() - - # Заполняем таблицу реагентами из модели - for reagent in self.model.reagents: - row = self.view.table.rowCount() - self.view.table.insertRow(row) - - self.view.table.setItem(row, 0, QTableWidgetItem(reagent.name)) - self.view.table.setItem(row, 1, QTableWidgetItem(f"{reagent.percentage:.2f}")) - - # Единица - QComboBox - unit_combo = QComboBox() - unit_combo.addItems(["нг", "мкг", "мг", "г", "кг", "нл", "мкл", "мл", "л"]) - unit_combo.setCurrentText(reagent.unit) - self.view.table.setCellWidget(row, 2, unit_combo) - - self.view.table.setItem(row, 3, QTableWidgetItem(f"{reagent.conversion_factor:.2f}")) - - # Разбавление - обычная ячейка - self.view.table.setItem(row, 4, QTableWidgetItem(f"{getattr(reagent, 'dilution_factor', 1.0):.3f}")) - - self.view.table.setItem(row, 5, QTableWidgetItem("")) - - # Очищаем результаты - self.view.clear_results() + def save_composition(self): - """Сохраняет состав среды в JSON-файл""" - filename, _ = QFileDialog.getSaveFileName( - self.view, - "Сохранить состав среды", - "", - "JSON Files (*.json);;All Files (*)" - ) - + filename, _ = QFileDialog.getSaveFileName(self.view, "Сохранить состав среды", "", "JSON Files (*.json);;All Files (*)") if filename: if not filename.lower().endswith('.json'): filename += '.json' - try: self._update_model_from_view() self.model.save_to_file(filename) QMessageBox.information(self.view, "Успех", "Состав среды успешно сохранён!") except Exception as e: self.view.show_error(f"Ошибка сохранения: {str(e)}") - + def load_composition(self): - """Загружает состав среды из JSON-файла""" - filename, _ = QFileDialog.getOpenFileName( - self.view, - "Загрузить состав среды", - "", - "JSON Files (*.json);;All Files (*)" - ) - + filename, _ = QFileDialog.getOpenFileName(self.view, "Загрузить состав среды", "", "JSON Files (*.json);;All Files (*)") if filename: try: self.model.load_from_file(filename) self._update_view_from_model() - QMessageBox.information( - self.view, - "Успех", - "Состав среды успешно загружен" - ) + QMessageBox.information(self.view, "Успех", "Состав среды успешно загружен") except FileNotFoundError: - QMessageBox.critical( - self.view, - "Ошибка", - f"Файл не найден: {filename}" - ) + QMessageBox.critical(self.view, "Ошибка", f"Файл не найден: {filename}") except json.JSONDecodeError as e: - QMessageBox.critical( - self.view, - "Ошибка", - f"Неверный формат JSON-файла: {str(e)}" - ) + QMessageBox.critical(self.view, "Ошибка", f"Неверный формат JSON-файла: {str(e)}") except Exception as e: - QMessageBox.critical( - self.view, - "Ошибка", - f"Ошибка при загрузке состава: {str(e)}" - ) + QMessageBox.critical(self.view, "Ошибка", f"Ошибка при загрузке состава: {str(e)}") + + def _update_view_from_model(self): + while self.view.table.rowCount() > 1: + self.view.table.removeRow(1) + if self.view.table.rowCount() == 0: + self.view.add_solvent_row() + self.view.amount_input.setValue(self.model.total_amount) + index = self.view.amount_unit_combo.findText(self.model.amount_unit) + if index >= 0: + self.view.amount_unit_combo.setCurrentIndex(index) + self.view.solvent_input.setText(self.model.solvent) + self.view.update_solvent_name() + for reagent in self.model.reagents: + row = self.view.table.rowCount() + self.view.table.insertRow(row) + self.view.table.setItem(row, 0, QTableWidgetItem(reagent.name)) + self.view.table.setItem(row, 1, QTableWidgetItem(f"{reagent.percentage:.2f}")) + unit_combo = QComboBox() + unit_combo.addItems(["нг", "мкг", "мг", "г", "кг", "нл", "мкл", "мл", "л"]) + unit_combo.setCurrentText(reagent.unit) + self.view.table.setCellWidget(row, 2, unit_combo) + self.view.table.setItem(row, 3, QTableWidgetItem(f"{reagent.conversion_factor:.2f}")) + self.view.table.setItem(row, 4, QTableWidgetItem(f"{getattr(reagent, 'dilution_factor', 1.0):.3f}")) + self.view.table.setItem(row, 5, QTableWidgetItem("")) + self.view.clear_results() diff --git a/src/models/__init__.py b/src/models/__init__.py new file mode 100644 index 0000000..3bcdcad --- /dev/null +++ b/src/models/__init__.py @@ -0,0 +1,5 @@ +from .reagent import Reagent +from .medium_model import MediumModel +from .experiment_model import ExperimentModel + +__all__ = ['Reagent', 'MediumModel', 'ExperimentModel'] diff --git a/src/models/experiment_model.py b/src/models/experiment_model.py new file mode 100644 index 0000000..fe5d322 --- /dev/null +++ b/src/models/experiment_model.py @@ -0,0 +1,80 @@ +import numpy as np +from typing import List, Dict, Any + +class ExperimentModel: + def __init__(self): + self.factors = [] + self.responses = [] + self.center_points = 3 + self.randomize = True + + def set_factors(self, factors: List[Dict]): + self.factors = factors + + def set_responses(self, responses: List[Dict]): + self.responses = responses + + def set_center_points(self, n: int): + self.center_points = n + + def set_randomize(self, value: bool): + self.randomize = value + + def calculate_factorial_design(self) -> List[Dict]: + k = len(self.factors) + if k == 0: + return [] + n_factorial = 2 ** k + design = [] + for i in range(n_factorial): + experiment = {} + for j in range(k): + coded_level = -1 if (i >> (k - 1 - j)) & 1 == 0 else 1 + natural_value = self.factors[j]['low'] if coded_level == -1 else self.factors[j]['high'] + experiment[f"Фактор_{j+1}"] = { + 'coded': coded_level, + 'natural': natural_value, + 'name': self.factors[j]['name'], + 'unit': self.factors[j]['unit'] + } + design.append(experiment) + for i in range(self.center_points): + center_experiment = {} + for j in range(k): + center_experiment[f"Фактор_{j+1}"] = { + 'coded': 0, + 'natural': self.factors[j]['center'], + 'name': self.factors[j]['name'], + 'unit': self.factors[j]['unit'] + } + center_experiment['is_center'] = True + center_experiment['center_num'] = i + 1 + design.append(center_experiment) + if self.randomize: + import random + random.shuffle(design) + return design + + def analyze_results(self, results: List[List[float]], design: List[Dict]) -> Dict: + analysis = {} + for resp_idx, response in enumerate(self.responses): + resp_name = response.get('name', f'Отклик_{resp_idx+1}') + y_values = [results[i][resp_idx] for i in range(len(results))] + mean_y = np.mean(y_values) + variance = np.var(y_values, ddof=1) if len(y_values) > 1 else 0 + std_dev = np.std(y_values, ddof=1) if len(y_values) > 1 else 0 + cv = (std_dev / mean_y) * 100 if mean_y != 0 else 0 + factorial_y = [] + center_y = [] + for i, exp in enumerate(design): + if exp.get('is_center', False): + center_y.append(y_values[i]) + else: + factorial_y.append(y_values[i]) + center_variance = np.var(center_y, ddof=1) if len(center_y) > 1 else 0 + analysis[resp_name] = { + 'mean': mean_y, 'variance': variance, 'std_dev': std_dev, 'cv': cv, + 'factorial_values': factorial_y, 'center_values': center_y, 'center_variance': center_variance, + 'n_factorial': len(factorial_y), 'n_center': len(center_y) + } + return analysis diff --git a/src/models/medium_model.py b/src/models/medium_model.py new file mode 100644 index 0000000..48e91f8 --- /dev/null +++ b/src/models/medium_model.py @@ -0,0 +1,113 @@ +import json +from typing import List, Tuple +from .reagent import Reagent + +VOLUME_UNITS = { + 'нл': 0.001, 'мкл': 1.0, 'мл': 1000.0, 'л': 1000000.0 +} + +MASS_UNITS = { + 'нг': 0.000001, 'мкг': 0.001, 'мг': 1.0, 'г': 1000.0, 'кг': 1000000.0 +} + +class MediumModel: + def __init__(self): + self.total_amount = 100.0 + self.amount_unit = 'мл' + self.solvent = 'Вода' + self.reagents = [] + + def convert_amount(self, amount_base: float, target_unit: str, is_volume: bool) -> float: + if is_volume: + conversion_factor = VOLUME_UNITS.get(target_unit, 1.0) + else: + conversion_factor = MASS_UNITS.get(target_unit, 1.0) + return amount_base / conversion_factor + + def calculate_amounts(self) -> Tuple[List[float], float, float]: + results = [] + if not self.reagents: + return results, self.total_amount, 100.0 + + total_percentage = sum(r.percentage for r in self.reagents) + if total_percentage > 100: + raise ValueError(f"Сумма процентов реагентов ({total_percentage:.2f}%) превышает 100%") + + total_in_base = self.total_amount * VOLUME_UNITS[self.amount_unit] + + undiluted_amounts = [] + for reagent in self.reagents: + amount_in_base = (reagent.percentage / 100) * total_in_base + is_volume = reagent.unit in VOLUME_UNITS + adjusted_amount = amount_in_base * reagent.conversion_factor + final_amount = self.convert_amount(adjusted_amount, reagent.unit, is_volume) + undiluted_amounts.append(final_amount) + + diluted_amounts = [] + total_diluted_volume_base = 0 + for i, reagent in enumerate(self.reagents): + dilution_factor = getattr(reagent, 'dilution_factor', 1.0) + if dilution_factor <= 0: + dilution_factor = 1.0 + diluted_amount = undiluted_amounts[i] * dilution_factor + diluted_amounts.append(diluted_amount) + is_volume = reagent.unit in VOLUME_UNITS + if is_volume: + reagent_volume_base = diluted_amount * VOLUME_UNITS[reagent.unit] + else: + reagent_volume_base = 0 + total_diluted_volume_base += reagent_volume_base + + solvent_volume_base = total_in_base - total_diluted_volume_base + solvent_amount = solvent_volume_base / VOLUME_UNITS[self.amount_unit] + if solvent_amount < 0: + solvent_amount = 0 + solvent_percentage = 100 - total_percentage + + return diluted_amounts, solvent_amount, solvent_percentage + + def save_to_file(self, filename: str): + data = { + 'total_amount': self.total_amount, + 'amount_unit': self.amount_unit, + 'solvent': self.solvent, + 'reagents': [{'name': r.name, 'percentage': r.percentage, 'unit': r.unit, + 'conversion_factor': r.conversion_factor, 'dilution_factor': getattr(r, 'dilution_factor', 1.0)} + for r in self.reagents] + } + with open(filename, 'w', encoding='utf-8') as f: + json.dump(data, f, ensure_ascii=False, indent=4) + + def load_from_file(self, filename: str): + with open(filename, 'r', encoding='utf-8') as f: + data = json.load(f) + self.total_amount = data['total_amount'] + self.amount_unit = data['amount_unit'] + self.solvent = data['solvent'] + self.reagents.clear() + for r_data in data['reagents']: + reagent = Reagent(r_data['name'], r_data['percentage'], r_data['unit'], r_data.get('conversion_factor', 1.0)) + reagent.dilution_factor = r_data.get('dilution_factor', 1.0) + self.reagents.append(reagent) + + def add_reagent(self, name: str, percentage: float, unit: str, conversion_factor: float = 1.0, dilution_factor: float = 1.0): + reagent = Reagent(name, percentage, unit, conversion_factor) + reagent.dilution_factor = dilution_factor + self.reagents.append(reagent) + + def remove_reagent(self, index: int): + if 0 <= index < len(self.reagents): + del self.reagents[index] + + def clear_reagents(self): + self.reagents.clear() + + def get_reagent_count(self) -> int: + return len(self.reagents) + + def set_total_amount(self, amount: float, unit: str): + self.total_amount = amount + self.amount_unit = unit + + def set_solvent(self, solvent_name: str): + self.solvent = solvent_name diff --git a/src/models/reagent.py b/src/models/reagent.py new file mode 100644 index 0000000..160892b --- /dev/null +++ b/src/models/reagent.py @@ -0,0 +1,10 @@ +class Reagent: + def __init__(self, name: str, percentage: float, unit: str, conversion_factor: float = 1.0): + self.name = name + self.percentage = percentage + self.unit = unit + self.conversion_factor = conversion_factor + self.dilution_factor = 1.0 + + def __repr__(self): + return f"Reagent(name={self.name}, percentage={self.percentage}, unit={self.unit})" diff --git a/src/utils/__init__.py b/src/utils/__init__.py new file mode 100644 index 0000000..a42179a --- /dev/null +++ b/src/utils/__init__.py @@ -0,0 +1,3 @@ +"""Утилиты и вспомогательные функции""" + +__all__ = [] diff --git a/src/views/__init__.py b/src/views/__init__.py new file mode 100644 index 0000000..e6fcdd9 --- /dev/null +++ b/src/views/__init__.py @@ -0,0 +1,5 @@ +from .main_window import MainWindow +from .medium_view import MediumCalculatorWindow +from .experiment_view import ExperimentDesignWindow + +__all__ = ['MainWindow', 'MediumCalculatorWindow', 'ExperimentDesignWindow'] diff --git a/experiment_design.py b/src/views/experiment_view.py similarity index 64% rename from experiment_design.py rename to src/views/experiment_view.py index a6b4889..8d6b2a6 100644 --- a/experiment_design.py +++ b/src/views/experiment_view.py @@ -1,14 +1,12 @@ -from PyQt5.QtWidgets import (QMainWindow, QVBoxLayout, QHBoxLayout, - QPushButton, QLabel, QWidget, QMessageBox, - QTableWidget, QTableWidgetItem, QGroupBox, - QSpinBox, QDoubleSpinBox, QComboBox, QLineEdit, - QTextEdit, QTabWidget, QFormLayout, QCheckBox, - QScrollArea, QFileDialog) +from PyQt5.QtWidgets import (QMainWindow, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, + QWidget, QMessageBox, QTableWidget, QTableWidgetItem, QGroupBox, + QSpinBox, QDoubleSpinBox, QComboBox, QLineEdit, QTextEdit, + QTabWidget, QFormLayout, QCheckBox, QScrollArea, QFileDialog) from PyQt5.QtCore import Qt from PyQt5.QtGui import QColor, QFont -import numpy as np import csv - +import random +import numpy as np class ExperimentDesignWindow(QMainWindow): def __init__(self): @@ -16,53 +14,24 @@ class ExperimentDesignWindow(QMainWindow): self.setWindowTitle("Планирование эксперимента - Цифровой помощник биохимика") self.setGeometry(200, 100, 1200, 800) self.setStyleSheet(""" - QMainWindow { - background-color: #f5f5f5; - } - QGroupBox { - font-weight: bold; - border: 2px solid #ccc; - border-radius: 5px; - margin-top: 10px; - padding-top: 10px; - } - QGroupBox::title { - subcontrol-origin: margin; - left: 10px; - padding: 0 5px 0 5px; - color: #1565C0; - } - QPushButton { - background-color: #4CAF50; - color: white; - border: none; - padding: 8px; - border-radius: 4px; - font-weight: bold; - } - QPushButton:hover { - background-color: #45a049; - } - QTableWidget { - gridline-color: #ddd; - } - QHeaderView::section { - background-color: #1565C0; - color: white; - padding: 8px; - border: none; - font-weight: bold; - } + QMainWindow { background-color: #f5f5f5; } + QGroupBox { font-weight: bold; border: 2px solid #ccc; border-radius: 5px; margin-top: 10px; padding-top: 10px; } + QGroupBox::title { subcontrol-origin: margin; left: 10px; padding: 0 5px 0 5px; color: #1565C0; } + QPushButton { background-color: #4CAF50; color: white; border: none; padding: 8px; border-radius: 4px; font-weight: bold; } + QPushButton:hover { background-color: #45a049; } + QPushButton#danger { background-color: #f44336; } + QPushButton#danger:hover { background-color: #da190b; } + QTableWidget { gridline-color: #ddd; } + QHeaderView::section { background-color: #1565C0; color: white; padding: 8px; font-weight: bold; } """) self._init_ui() - + def _init_ui(self): central_widget = QWidget() self.setCentralWidget(central_widget) layout = QVBoxLayout(central_widget) - - # Заголовок - title_label = QLabel("📈 Планирование полнофакторного эксперимента (DoE)") + + title_label = QLabel("Планирование полнофакторного эксперимента (DoE)") title_font = QFont() title_font.setPointSize(18) title_font.setBold(True) @@ -70,15 +39,13 @@ class ExperimentDesignWindow(QMainWindow): title_label.setAlignment(Qt.AlignCenter) title_label.setStyleSheet("color: #2E7D32;") layout.addWidget(title_label) - - # Вкладки + tabs = QTabWidget() - # Вкладка 1: Параметры эксперимента + # Вкладка параметров params_tab = QWidget() params_layout = QVBoxLayout(params_tab) - # Группа факторов factors_group = QGroupBox("Факторы эксперимента (независимые переменные)") factors_layout = QVBoxLayout() @@ -87,27 +54,20 @@ class ExperimentDesignWindow(QMainWindow): factors_layout.addWidget(info_label) self.factors_table = QTableWidget() - # Новый порядок колонок: Фактор, Нулевой уровень, Шаг, Верхний (+1), Нижний (-1), Единица измерения self.factors_table.setColumnCount(6) self.factors_table.setHorizontalHeaderLabels(["Фактор", "Нулевой уровень (0)", "Шаг", "Верхний уровень (+1)", "Нижний уровень (-1)", "Единица измерения"]) self.factors_table.setRowCount(2) - # Пример данных - sample_factors = [ - ["Температура", "31", "6", "37", "25", "°C"], - ["pH", "7.0", "0.5", "7.5", "6.5", ""], - ] - + sample_factors = [["Температура", "31", "6", "37", "25", "°C"], ["pH", "7.0", "0.5", "7.5", "6.5", ""]] for i, factor in enumerate(sample_factors): for j, value in enumerate(factor): item = QTableWidgetItem(value) - if j in [3, 4]: # Верхний и нижний уровень - только для чтения + if j in [3, 4]: item.setFlags(item.flags() & ~Qt.ItemIsEditable) item.setBackground(QColor(240, 240, 240)) self.factors_table.setItem(i, j, item) - # Настройка ширины колонок self.factors_table.setColumnWidth(0, 150) self.factors_table.setColumnWidth(1, 120) self.factors_table.setColumnWidth(2, 80) @@ -115,12 +75,9 @@ class ExperimentDesignWindow(QMainWindow): self.factors_table.setColumnWidth(4, 120) self.factors_table.setColumnWidth(5, 100) - # Подключаем сигналы изменения ячеек self.factors_table.itemChanged.connect(self.on_factor_changed) - factors_layout.addWidget(self.factors_table) - # Кнопки для управления факторами factor_buttons = QHBoxLayout() add_factor_btn = QPushButton("+ Добавить фактор") add_factor_btn.clicked.connect(self.add_factor_row) @@ -130,49 +87,35 @@ class ExperimentDesignWindow(QMainWindow): factor_buttons.addWidget(remove_factor_btn) factor_buttons.addStretch() factors_layout.addLayout(factor_buttons) - factors_group.setLayout(factors_layout) params_layout.addWidget(factors_group) - # Группа настроек эксперимента settings_group = QGroupBox("Настройки эксперимента") settings_layout = QHBoxLayout() - center_layout = QHBoxLayout() center_layout.addWidget(QLabel("Количество центральных точек:")) self.center_points_spin = QSpinBox() self.center_points_spin.setRange(0, 10) self.center_points_spin.setValue(3) - self.center_points_spin.setToolTip("Повторные опыты в нулевой точке для оценки дисперсии") center_layout.addWidget(self.center_points_spin) settings_layout.addLayout(center_layout) - self.randomize_check = QCheckBox("Рэндомизировать порядок опытов") self.randomize_check.setChecked(True) settings_layout.addWidget(self.randomize_check) - settings_layout.addStretch() settings_group.setLayout(settings_layout) params_layout.addWidget(settings_group) - # Группа откликов responses_group = QGroupBox("Отклики (зависимые переменные)") responses_layout = QVBoxLayout() - self.responses_table = QTableWidget() self.responses_table.setColumnCount(2) self.responses_table.setHorizontalHeaderLabels(["Отклик", "Единица измерения"]) self.responses_table.setRowCount(2) - - sample_responses = [ - ["Оптическая плотность (OD600)", ""], - ["Концентрация целевого продукта", "мг/мл"] - ] - + sample_responses = [["Оптическая плотность (OD600)", ""], ["Концентрация целевого продукта", "мг/мл"]] for i, response in enumerate(sample_responses): for j, value in enumerate(response): self.responses_table.setItem(i, j, QTableWidgetItem(value)) - responses_layout.addWidget(self.responses_table) response_buttons = QHBoxLayout() @@ -184,16 +127,13 @@ class ExperimentDesignWindow(QMainWindow): response_buttons.addWidget(remove_response_btn) response_buttons.addStretch() responses_layout.addLayout(response_buttons) - responses_group.setLayout(responses_layout) params_layout.addWidget(responses_group) - tabs.addTab(params_tab, "📝 Параметры эксперимента") - # Вкладка 2: Матрица планирования + # Вкладка матрицы планирования plan_tab = QWidget() plan_layout = QVBoxLayout(plan_tab) - plan_info = QLabel("Полнофакторный план эксперимента с центральными точками") plan_info.setAlignment(Qt.AlignCenter) plan_info.setStyleSheet("font-weight: bold; font-size: 14px; padding: 10px;") @@ -203,145 +143,105 @@ class ExperimentDesignWindow(QMainWindow): scroll.setWidgetResizable(True) matrix_widget = QWidget() matrix_layout = QVBoxLayout(matrix_widget) - self.design_matrix = QTableWidget() matrix_layout.addWidget(self.design_matrix) - scroll.setWidget(matrix_widget) plan_layout.addWidget(scroll) buttons_layout = QHBoxLayout() - - generate_btn = QPushButton("🔄 Сгенерировать план эксперимента") + generate_btn = QPushButton("Сгенерировать план эксперимента") generate_btn.clicked.connect(self.generate_design_matrix) buttons_layout.addWidget(generate_btn) - export_btn = QPushButton("📊 Экспорт в CSV") export_btn.clicked.connect(self.export_to_csv) buttons_layout.addWidget(export_btn) - buttons_layout.addStretch() plan_layout.addLayout(buttons_layout) self.plan_info_label = QLabel("") self.plan_info_label.setStyleSheet("color: #666; font-size: 12px; padding: 5px;") plan_layout.addWidget(self.plan_info_label) - tabs.addTab(plan_tab, "📊 Матрица планирования") - # Вкладка 3: Анализ результатов + # Вкладка анализа analysis_tab = QWidget() analysis_layout = QVBoxLayout(analysis_tab) - analysis_info = QLabel("Введите результаты экспериментов для анализа") analysis_info.setAlignment(Qt.AlignCenter) analysis_info.setStyleSheet("font-weight: bold; font-size: 14px; padding: 10px;") analysis_layout.addWidget(analysis_info) - self.results_table = QTableWidget() analysis_layout.addWidget(self.results_table) - - analyze_btn = QPushButton("📈 Провести регрессионный анализ") + analyze_btn = QPushButton("Провести регрессионный анализ") analyze_btn.clicked.connect(self.perform_analysis) analysis_layout.addWidget(analyze_btn) - self.analysis_output = QTextEdit() self.analysis_output.setReadOnly(True) self.analysis_output.setMaximumHeight(200) analysis_layout.addWidget(self.analysis_output) - - tabs.addTab(analysis_tab, "📐 Анализ результатов") + tabs.addTab(analysis_tab, "📈 Анализ результатов") layout.addWidget(tabs) btn_layout = QHBoxLayout() - - save_btn = QPushButton("💿 Сохранить проект") - save_btn.clicked.connect(self.show_placeholder_message) - btn_layout.addWidget(save_btn) - - load_btn = QPushButton("📂 Загрузить проект") - load_btn.clicked.connect(self.show_placeholder_message) - btn_layout.addWidget(load_btn) - - btn_layout.addStretch() - - close_btn = QPushButton("❌ Закрыть") + close_btn = QPushButton("Закрыть") close_btn.clicked.connect(self.close) + btn_layout.addStretch() btn_layout.addWidget(close_btn) - layout.addLayout(btn_layout) - + + self.generated_design = None + self.factors_data = None + def on_factor_changed(self, item): - """При изменении нулевого уровня или шага пересчитываем верхний и нижний уровни""" row = item.row() col = item.column() - - # Если изменили нулевой уровень (колонка 1) или шаг (колонка 2) if col in [1, 2]: try: center = float(self.factors_table.item(row, 1).text()) if self.factors_table.item(row, 1) else 0 step = float(self.factors_table.item(row, 2).text()) if self.factors_table.item(row, 2) else 0 - - # Пересчитываем верхний и нижний уровни high = center + step low = center - step - - # Обновляем ячейки (временно отключаем сигнал) self.factors_table.blockSignals(True) - - high_item = self.factors_table.item(row, 3) - if high_item: - high_item.setText(f"{high:.3f}".rstrip('0').rstrip('.')) - - low_item = self.factors_table.item(row, 4) - if low_item: - low_item.setText(f"{low:.3f}".rstrip('0').rstrip('.')) - + if self.factors_table.item(row, 3): + self.factors_table.item(row, 3).setText(f"{high:.3f}".rstrip('0').rstrip('.')) + if self.factors_table.item(row, 4): + self.factors_table.item(row, 4).setText(f"{low:.3f}".rstrip('0').rstrip('.')) self.factors_table.blockSignals(False) except ValueError: pass - + def add_factor_row(self): - """Добавляет строку для нового фактора""" row = self.factors_table.rowCount() self.factors_table.insertRow(row) self.factors_table.setItem(row, 0, QTableWidgetItem(f"Фактор_{row+1}")) self.factors_table.setItem(row, 1, QTableWidgetItem("0")) self.factors_table.setItem(row, 2, QTableWidgetItem("1")) - - # Верхний и нижний уровни - только для чтения high_item = QTableWidgetItem("1") high_item.setFlags(high_item.flags() & ~Qt.ItemIsEditable) high_item.setBackground(QColor(240, 240, 240)) self.factors_table.setItem(row, 3, high_item) - low_item = QTableWidgetItem("-1") low_item.setFlags(low_item.flags() & ~Qt.ItemIsEditable) low_item.setBackground(QColor(240, 240, 240)) self.factors_table.setItem(row, 4, low_item) - self.factors_table.setItem(row, 5, QTableWidgetItem("")) - + def remove_factor_row(self): - """Удаляет последнюю строку факторов""" if self.factors_table.rowCount() > 1: self.factors_table.removeRow(self.factors_table.rowCount() - 1) - + def add_response_row(self): - """Добавляет строку для нового отклика""" row = self.responses_table.rowCount() self.responses_table.insertRow(row) self.responses_table.setItem(row, 0, QTableWidgetItem(f"Отклик_{row+1}")) self.responses_table.setItem(row, 1, QTableWidgetItem("")) - + def remove_response_row(self): - """Удаляет последнюю строку откликов""" if self.responses_table.rowCount() > 1: self.responses_table.removeRow(self.responses_table.rowCount() - 1) - + def get_factors_data(self): - """Получает данные о факторах""" factors = [] for row in range(self.factors_table.rowCount()): try: @@ -357,29 +257,18 @@ class ExperimentDesignWindow(QMainWindow): except (ValueError, AttributeError): continue return factors - + def calculate_factorial_design(self, factors): - """Генерирует полнофакторный план 2^k с центральными точками""" k = len(factors) if k == 0: return [] - - # Генерируем 2^k комбинаций n_factorial = 2 ** k design = [] - for i in range(n_factorial): experiment = {} for j in range(k): - # Кодированный уровень (-1 или +1) coded_level = -1 if (i >> (k - 1 - j)) & 1 == 0 else 1 - - # Переводим в натуральные значения - if coded_level == -1: - natural_value = factors[j]['low'] - else: - natural_value = factors[j]['high'] - + natural_value = factors[j]['low'] if coded_level == -1 else factors[j]['high'] experiment[f"Фактор_{j+1}"] = { 'coded': coded_level, 'natural': natural_value, @@ -388,7 +277,6 @@ class ExperimentDesignWindow(QMainWindow): } design.append(experiment) - # Добавляем центральные точки n_center = self.center_points_spin.value() for i in range(n_center): center_experiment = {} @@ -403,48 +291,34 @@ class ExperimentDesignWindow(QMainWindow): center_experiment['center_num'] = i + 1 design.append(center_experiment) - # Рэндомизация порядка опытов if self.randomize_check.isChecked(): - import random random.shuffle(design) - return design - + def generate_design_matrix(self): - """Генерирует и отображает матрицу планирования""" factors = self.get_factors_data() - if len(factors) == 0: QMessageBox.warning(self, "Предупреждение", "Добавьте хотя бы один фактор!") return - # Генерируем план + self.factors_data = factors design = self.calculate_factorial_design(factors) + self.generated_design = design - # Количество опытов n_experiments = len(design) n_factors = len(factors) - # Настройка таблицы self.design_matrix.setRowCount(n_experiments) self.design_matrix.setColumnCount(n_factors + 2) - - # Заголовки headers = ["№ опыта"] + [f["name"] for f in factors] + ["Тип точки"] self.design_matrix.setHorizontalHeaderLabels(headers) - # Заполняем матрицу for exp_idx, experiment in enumerate(design): - # Номер опыта self.design_matrix.setItem(exp_idx, 0, QTableWidgetItem(str(exp_idx + 1))) - - # Значения факторов for factor_idx in range(n_factors): factor_key = f"Фактор_{factor_idx + 1}" value = experiment[factor_key]['natural'] unit = factors[factor_idx]['unit'] - - # Форматируем значение if isinstance(value, float): if value == int(value): display_value = str(int(value)) @@ -452,123 +326,76 @@ class ExperimentDesignWindow(QMainWindow): display_value = f"{value:.3f}".rstrip('0').rstrip('.') else: display_value = str(value) - if unit: display_value += f" {unit}" - item = QTableWidgetItem(display_value) - - # Подсветка центральных точек if experiment.get('is_center', False): item.setBackground(QColor(255, 255, 200)) - self.design_matrix.setItem(exp_idx, factor_idx + 1, item) - # Тип точки if experiment.get('is_center', False): type_item = QTableWidgetItem(f"Центральная #{experiment['center_num']}") type_item.setBackground(QColor(255, 255, 200)) else: - # Показываем комбинацию уровней levels = [] for factor_idx in range(n_factors): factor_key = f"Фактор_{factor_idx + 1}" coded = experiment[factor_key]['coded'] levels.append("+" if coded == 1 else "-") type_item = QTableWidgetItem(f"Факторная ({''.join(levels)})") - self.design_matrix.setItem(exp_idx, n_factors + 1, type_item) - # Настройка ширины колонок self.design_matrix.resizeColumnsToContents() - - # Обновляем информацию n_factorial = 2 ** n_factors n_center = self.center_points_spin.value() - self.plan_info_label.setText( - f"📊 План эксперимента: {n_factorial} факторных точек + {n_center} центральных точек = {n_experiments} опытов" - ) - - # Настраиваем таблицу для ввода результатов + self.plan_info_label.setText(f"📊 План эксперимента: {n_factorial} факторных точек + {n_center} центральных точек = {n_experiments} опытов") self.setup_results_table(n_experiments) - - QMessageBox.information(self, "Успех", - f"Сгенерирован план для {n_factors} факторов\n" - f"Факторных точек: {n_factorial}\n" - f"Центральных точек: {n_center}\n" - f"Всего опытов: {n_experiments}\n\n" - f"Центральные точки позволяют оценить дисперсию воспроизводимости") - + QMessageBox.information(self, "Успех", f"Сгенерирован план для {n_factors} факторов\nФакторных точек: {n_factorial}\nЦентральных точек: {n_center}\nВсего опытов: {n_experiments}") + def setup_results_table(self, n_experiments): - """Настраивает таблицу для ввода результатов""" n_responses = self.responses_table.rowCount() - self.results_table.setRowCount(n_experiments) self.results_table.setColumnCount(n_responses + 1) - - # Заголовки - headers = ["№ опыта"] + [self.responses_table.item(i, 0).text() if self.responses_table.item(i, 0) else f"Отклик_{i+1}" - for i in range(n_responses)] + headers = ["№ опыта"] + [self.responses_table.item(i, 0).text() if self.responses_table.item(i, 0) else f"Отклик_{i+1}" for i in range(n_responses)] self.results_table.setHorizontalHeaderLabels(headers) - - # Заполняем номера опытов for i in range(n_experiments): self.results_table.setItem(i, 0, QTableWidgetItem(str(i + 1))) - - # Настройка ширины колонок self.results_table.setColumnWidth(0, 80) for i in range(n_responses): self.results_table.setColumnWidth(i + 1, 150) - + def export_to_csv(self): - """Экспортирует матрицу планирования в CSV""" if self.design_matrix.rowCount() == 0: QMessageBox.warning(self, "Предупреждение", "Сначала сгенерируйте план эксперимента!") return - - filename, _ = QFileDialog.getSaveFileName( - self, - "Сохранить план эксперимента", - "", - "CSV Files (*.csv);;All Files (*)" - ) - + filename, _ = QFileDialog.getSaveFileName(self, "Сохранить план эксперимента", "", "CSV Files (*.csv);;All Files (*)") if filename: if not filename.lower().endswith('.csv'): filename += '.csv' - try: with open(filename, 'w', newline='', encoding='utf-8-sig') as f: writer = csv.writer(f) - - # Заголовки headers = [] for j in range(self.design_matrix.columnCount()): header_item = self.design_matrix.horizontalHeaderItem(j) headers.append(header_item.text() if header_item else f"Колонка_{j+1}") writer.writerow(headers) - - # Данные for i in range(self.design_matrix.rowCount()): - row = [] - for j in range(self.design_matrix.columnCount()): - item = self.design_matrix.item(i, j) - row.append(item.text() if item else "") + row = [self.design_matrix.item(i, j).text() if self.design_matrix.item(i, j) else "" for j in range(self.design_matrix.columnCount())] writer.writerow(row) - QMessageBox.information(self, "Успех", f"План эксперимента сохранен в {filename}") except Exception as e: QMessageBox.critical(self, "Ошибка", f"Не удалось сохранить файл: {str(e)}") - + def perform_analysis(self): - """Проводит регрессионный анализ""" n_responses = self.responses_table.rowCount() - if n_responses == 0: QMessageBox.warning(self, "Предупреждение", "Добавьте хотя бы один отклик!") return + if self.generated_design is None: + QMessageBox.warning(self, "Предупреждение", "Сначала сгенерируйте план эксперимента!") + return - # Собираем результаты results = [] for i in range(self.results_table.rowCount()): row_results = [] @@ -583,50 +410,36 @@ class ExperimentDesignWindow(QMainWindow): row_results.append(None) results.append(row_results) - # Проверяем, что все результаты введены - missing = False for i, row in enumerate(results): for j, val in enumerate(row): if val is None: - missing = True - self.analysis_output.setText(f"❌ Ошибка: Не введены результаты для опыта {i+1}, отклик {j+1}") + self.analysis_output.setText(f"Ошибка: Не введены результаты для опыта {i+1}, отклик {j+1}") return - # Анализ self.analysis_output.clear() self.analysis_output.append("=" * 60) self.analysis_output.append("РЕЗУЛЬТАТЫ РЕГРЕССИОННОГО АНАЛИЗА") self.analysis_output.append("=" * 60) factors = self.get_factors_data() - design = self.calculate_factorial_design(factors) + design = self.generated_design for resp_idx in range(n_responses): resp_name = self.responses_table.item(resp_idx, 0).text() self.analysis_output.append(f"\n📊 Отклик: {resp_name}") self.analysis_output.append("-" * 40) - # Собираем значения отклика y_values = [results[i][resp_idx] for i in range(len(results))] - - # Среднее значение mean_y = np.mean(y_values) - self.analysis_output.append(f"Среднее значение: {mean_y:.4f}") - - # Дисперсия variance = np.var(y_values, ddof=1) if len(y_values) > 1 else 0 - self.analysis_output.append(f"Общая дисперсия: {variance:.4f}") - - # Стандартное отклонение std_dev = np.std(y_values, ddof=1) if len(y_values) > 1 else 0 + cv = (std_dev / mean_y) * 100 if mean_y != 0 else 0 + + self.analysis_output.append(f"Среднее значение: {mean_y:.4f}") + self.analysis_output.append(f"Общая дисперсия: {variance:.4f}") self.analysis_output.append(f"Стандартное отклонение: {std_dev:.4f}") + self.analysis_output.append(f"Коэффициент вариации: {cv:.2f}%") - # Коэффициент вариации - if mean_y != 0: - cv = (std_dev / mean_y) * 100 - self.analysis_output.append(f"Коэффициент вариации: {cv:.2f}%") - - # Разделяем факторные и центральные точки factorial_y = [] center_y = [] for i, exp in enumerate(design): @@ -636,34 +449,22 @@ class ExperimentDesignWindow(QMainWindow): factorial_y.append(y_values[i]) if len(center_y) > 1: - center_variance = np.var(center_y, ddof=1) + center_variance = np.var(center_y, ddof=1) if len(center_y) > 1 else 0 self.analysis_output.append(f"\nЦентральные точки (n={len(center_y)}):") self.analysis_output.append(f" Среднее: {np.mean(center_y):.4f}") self.analysis_output.append(f" Дисперсия воспроизводимости: {center_variance:.4f}") - self.analysis_output.append(f" Стандартное отклонение: {np.std(center_y, ddof=1):.4f}") - - # Критерий Фишера для проверки адекватности if len(factorial_y) > 0 and center_variance > 0: factorial_variance = np.var(factorial_y, ddof=1) if len(factorial_y) > 1 else 0 if factorial_variance > 0: fisher = max(factorial_variance, center_variance) / min(factorial_variance, center_variance) self.analysis_output.append(f"\nКритерий Фишера (F-отношение): {fisher:.4f}") if fisher < 4.0: - self.analysis_output.append(" ✅ Модель адекватна экспериментальным данным") + self.analysis_output.append("✅ Модель адекватна экспериментальным данным") else: - self.analysis_output.append(" ⚠️ Модель может быть неадекватна, требуется проверка") + self.analysis_output.append("⚠️ Модель может быть неадекватна, требуется проверка") self.analysis_output.append("\n" + "=" * 60) - self.analysis_output.append("✅ Анализ завершен") - - def show_placeholder_message(self): - """Показывает сообщение о том, что функция в разработке""" - QMessageBox.information( - self, - "В разработке", - "🧪 Функция в стадии разработки!\n\nБлижайшие обновления:\n" - "✅ Экспорт в Excel\n" - "✅ Построение поверхностей отклика\n" - "✅ Графики главных эффектов\n" - "✅ Полный регрессионный анализ" - ) + self.analysis_output.append("Анализ завершен") + + def show_error(self, message: str): + QMessageBox.critical(self, "Ошибка", message) diff --git a/main_window.py b/src/views/main_window.py similarity index 53% rename from main_window.py rename to src/views/main_window.py index aaa542c..85ab15f 100644 --- a/main_window.py +++ b/src/views/main_window.py @@ -1,10 +1,6 @@ -from PyQt5.QtWidgets import (QMainWindow, QVBoxLayout, QHBoxLayout, - QPushButton, QLabel, QWidget, QFrame) +from PyQt5.QtWidgets import (QMainWindow, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QWidget, QFrame) from PyQt5.QtCore import Qt from PyQt5.QtGui import QFont -from controller import Controller -from experiment_design import ExperimentDesignWindow - class MainWindow(QMainWindow): def __init__(self): @@ -12,42 +8,22 @@ class MainWindow(QMainWindow): self.setWindowTitle("Цифровой помощник биохимика - Главное меню") self.setGeometry(300, 200, 700, 500) self.setStyleSheet(""" - QMainWindow { - background-color: qlineargradient(x1:0, y1:0, x2:1, y2:1, - stop:0 #e8f4f8, stop:1 #f0f0f0); - } - QPushButton { - background-color: #2196F3; - color: white; - border: none; - padding: 15px; - font-size: 16px; - font-weight: bold; - border-radius: 8px; - font-family: 'Segoe UI', Arial; - } - QPushButton:hover { - background-color: #1976D2; - } - QPushButton:pressed { - background-color: #0D47A1; - } - QLabel { - color: #333; - font-size: 14px; - font-family: 'Segoe UI', Arial; - } + QMainWindow { background-color: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #e8f4f8, stop:1 #f0f0f0); } + QPushButton { background-color: #2196F3; color: white; border: none; padding: 15px; font-size: 16px; font-weight: bold; border-radius: 8px; } + QPushButton:hover { background-color: #1976D2; } + QLabel { color: #333; font-size: 14px; } """) self._init_ui() - + self.medium_calculator = None + self.experiment_window = None + def _init_ui(self): central_widget = QWidget() self.setCentralWidget(central_widget) layout = QVBoxLayout(central_widget) layout.setSpacing(20) layout.setContentsMargins(50, 50, 50, 50) - - # Заголовок + title_label = QLabel("Цифровой помощник биохимика") title_font = QFont() title_font.setPointSize(20) @@ -56,8 +32,7 @@ class MainWindow(QMainWindow): title_label.setAlignment(Qt.AlignCenter) title_label.setStyleSheet("color: #1565C0;") layout.addWidget(title_label) - - # Подзаголовок + subtitle_label = QLabel("Биотехнологические инструменты для лаборатории") subtitle_font = QFont() subtitle_font.setPointSize(12) @@ -65,81 +40,62 @@ class MainWindow(QMainWindow): subtitle_label.setAlignment(Qt.AlignCenter) subtitle_label.setStyleSheet("color: #666;") layout.addWidget(subtitle_label) - + layout.addSpacing(20) - - # Кнопка 1: Калькулятор питательных сред - btn_medium = QPushButton("Калькулятор питательных сред") + + btn_medium = QPushButton("🧪 Калькулятор питательных сред") btn_medium.setMinimumHeight(80) btn_medium.clicked.connect(self.open_medium_calculator) layout.addWidget(btn_medium) - - # Описание кнопки 1 - desc1_label = QLabel("Расчёт состава питательной среды с учётом процентного содержания,\n" - "разбавления реагентов и автоматическим расчётом растворителя") + + desc1_label = QLabel("Расчёт состава питательной среды с учётом процентного содержания,\nразбавления реагентов и автоматическим расчётом растворителя") desc1_label.setAlignment(Qt.AlignCenter) desc1_label.setWordWrap(True) desc1_label.setStyleSheet("color: #555; font-size: 11px;") layout.addWidget(desc1_label) - + layout.addSpacing(15) - - # Кнопка 2: Планирование эксперимента - btn_experiment = QPushButton("Планирование эксперимента (DoE)") + + btn_experiment = QPushButton("📊 Планирование эксперимента (DoE)") btn_experiment.setMinimumHeight(80) btn_experiment.clicked.connect(self.open_experiment_designer) layout.addWidget(btn_experiment) - - # Описание кнопки 2 - desc2_label = QLabel("Дизайн эксперимента, оптимизация процессов,\n" - "многомерный анализ и визуализация") + + desc2_label = QLabel("Дизайн эксперимента, оптимизация процессов,\nмногомерный анализ и визуализация") desc2_label.setAlignment(Qt.AlignCenter) desc2_label.setWordWrap(True) desc2_label.setStyleSheet("color: #555; font-size: 11px;") layout.addWidget(desc2_label) - + layout.addSpacing(15) - - # Линия-разделитель + line = QFrame() line.setFrameShape(QFrame.HLine) line.setFrameShadow(QFrame.Sunken) layout.addWidget(line) - - # Нижняя панель - bottom_layout = QHBoxLayout() - # Информация о версии - version_label = QLabel("Версия alpha 0.1.2 | © 2026 Цифровой помощник биохимика") + bottom_layout = QHBoxLayout() + version_label = QLabel("Версия 1.0.0 | © 2026 Цифровой помощник биохимика") version_label.setStyleSheet("color: #999; font-size: 10px;") bottom_layout.addWidget(version_label) - bottom_layout.addStretch() - - # Кнопка выхода btn_exit = QPushButton("Выход") btn_exit.setMaximumWidth(150) - btn_exit.setStyleSheet(""" - QPushButton { - background-color: #f44336; - padding: 8px; - font-size: 14px; - } - QPushButton:hover { - background-color: #da190b; - } - """) + btn_exit.setStyleSheet("QPushButton { background-color: #f44336; padding: 8px; font-size: 14px; } QPushButton:hover { background-color: #da190b; }") btn_exit.clicked.connect(self.close) bottom_layout.addWidget(btn_exit) - layout.addLayout(bottom_layout) - + def open_medium_calculator(self): - """Открывает калькулятор питательной среды""" - self.medium_calculator = Controller() - self.medium_calculator.view.show() - + from .medium_view import MediumCalculatorWindow + from ..controllers.medium_controller import MediumController + self.medium_calculator = MediumCalculatorWindow() + self.medium_controller = MediumController(self.medium_calculator) + self.medium_calculator.show() + def open_experiment_designer(self): - """Открывает окно планирования эксперимента""" + from .experiment_view import ExperimentDesignWindow + from ..controllers.experiment_controller import ExperimentController self.experiment_window = ExperimentDesignWindow() + self.experiment_controller = ExperimentController(self.experiment_window) self.experiment_window.show() diff --git a/src/views/medium_view.py b/src/views/medium_view.py new file mode 100644 index 0000000..f1f1c2f --- /dev/null +++ b/src/views/medium_view.py @@ -0,0 +1,207 @@ +from PyQt5.QtWidgets import (QMainWindow, QVBoxLayout, QHBoxLayout, QTableWidget, QTableWidgetItem, + QPushButton, QLabel, QDoubleSpinBox, QComboBox, QLineEdit, QWidget, + QMessageBox, QGroupBox, QFrame) +from PyQt5.QtCore import Qt +from PyQt5.QtGui import QFont, QColor + +class MediumCalculatorWindow(QMainWindow): + def __init__(self): + super().__init__() + self.setWindowTitle("Калькулятор питательных сред") + self.setGeometry(200, 100, 1200, 700) + self.setStyleSheet(""" + QMainWindow { background-color: #f5f5f5; } + QGroupBox { font-weight: bold; border: 2px solid #ccc; border-radius: 5px; margin-top: 10px; padding-top: 10px; background-color: white; } + QGroupBox::title { subcontrol-origin: margin; left: 10px; padding: 0 5px 0 5px; color: #1565C0; } + QPushButton { background-color: #2196F3; color: white; border: none; padding: 8px 15px; border-radius: 4px; font-weight: bold; } + QPushButton:hover { background-color: #1976D2; } + QPushButton#danger { background-color: #f44336; } + QPushButton#success { background-color: #4CAF50; } + QTableWidget { gridline-color: #ddd; background-color: white; } + QHeaderView::section { background-color: #1565C0; color: white; padding: 8px; font-weight: bold; } + """) + self._init_ui() + + def _init_ui(self): + central_widget = QWidget() + self.setCentralWidget(central_widget) + layout = QVBoxLayout(central_widget) + layout.setSpacing(15) + layout.setContentsMargins(20, 20, 20, 20) + + title_label = QLabel("Калькулятор питательных сред") + title_label.setAlignment(Qt.AlignCenter) + title_font = QFont() + title_font.setPointSize(18) + title_font.setBold(True) + title_label.setFont(title_font) + layout.addWidget(title_label) + + params_group = QGroupBox("Параметры среды") + params_layout = QHBoxLayout() + amount_layout = QHBoxLayout() + amount_layout.addWidget(QLabel("Общее количество:")) + self.amount_input = QDoubleSpinBox() + self.amount_input.setRange(0.001, 1000000.0) + self.amount_input.setValue(1000.0) + amount_layout.addWidget(self.amount_input) + self.amount_unit_combo = QComboBox() + self.amount_unit_combo.addItems(["нл", "мкл", "мл", "л"]) + self.amount_unit_combo.setCurrentText("мл") + amount_layout.addWidget(self.amount_unit_combo) + params_layout.addLayout(amount_layout) + solvent_layout = QHBoxLayout() + solvent_layout.addWidget(QLabel("Растворитель:")) + self.solvent_input = QLineEdit("Вода") + solvent_layout.addWidget(self.solvent_input) + params_layout.addLayout(solvent_layout) + params_layout.addStretch() + params_group.setLayout(params_layout) + layout.addWidget(params_group) + + table_group = QGroupBox("Состав среды") + table_layout = QVBoxLayout() + self.table = QTableWidget() + self.table.setColumnCount(6) + self.table.setHorizontalHeaderLabels(["Название", "%", "Единица", "Коэфф.", "Разбавление (x)", "Количество"]) + self.table.setAlternatingRowColors(True) + self.table.setSelectionBehavior(QTableWidget.SelectRows) + self.table.setColumnWidth(0, 180) + self.table.setColumnWidth(1, 70) + self.table.setColumnWidth(2, 90) + self.table.setColumnWidth(3, 70) + self.table.setColumnWidth(4, 100) + self.table.setColumnWidth(5, 120) + table_layout.addWidget(self.table) + table_group.setLayout(table_layout) + layout.addWidget(table_group) + + btn_group = QGroupBox("Управление") + btn_layout = QHBoxLayout() + self.add_row_btn = QPushButton("➕ Добавить реагент") + self.remove_row_btn = QPushButton("➖ Удалить реагент") + self.remove_row_btn.setObjectName("danger") + btn_layout.addWidget(self.add_row_btn) + btn_layout.addWidget(self.remove_row_btn) + btn_layout.addStretch() + self.calculate_btn = QPushButton("🧮 Рассчитать") + self.calculate_btn.setObjectName("success") + self.save_btn = QPushButton("💾 Сохранить") + self.load_btn = QPushButton("📂 Загрузить") + btn_layout.addWidget(self.calculate_btn) + btn_layout.addWidget(self.save_btn) + btn_layout.addWidget(self.load_btn) + btn_group.setLayout(btn_layout) + layout.addWidget(btn_group) + + info_frame = QFrame() + info_frame.setFrameShape(QFrame.StyledPanel) + info_frame.setStyleSheet("background-color: #e3f2fd; border-radius: 5px;") + info_layout = QHBoxLayout(info_frame) + info_label = QLabel("ℹ️ Подсказка: Реагенты в массовых единицах не учитываются при расчёте объёма растворителя") + info_layout.addWidget(info_label) + info_layout.addStretch() + layout.addWidget(info_frame) + + def add_initial_rows(self): + self.add_solvent_row() + self.add_new_row() + + def add_solvent_row(self): + row = self.table.rowCount() + self.table.insertRow(row) + solvent_item = QTableWidgetItem(self.solvent_input.text()) + solvent_item.setFlags(solvent_item.flags() & ~Qt.ItemIsEditable) + solvent_item.setBackground(QColor(230, 230, 230)) + font = QFont() + font.setBold(True) + solvent_item.setFont(font) + self.table.setItem(row, 0, solvent_item) + for col in [1, 3, 4]: + item = QTableWidgetItem("-") + item.setFlags(item.flags() & ~Qt.ItemIsEditable) + item.setBackground(QColor(230, 230, 230)) + self.table.setItem(row, col, item) + unit_item = QTableWidgetItem(self.amount_unit_combo.currentText()) + unit_item.setFlags(unit_item.flags() & ~Qt.ItemIsEditable) + unit_item.setBackground(QColor(230, 230, 230)) + self.table.setItem(row, 2, unit_item) + result_item = QTableWidgetItem("") + result_item.setFlags(result_item.flags() & ~Qt.ItemIsEditable) + self.table.setItem(row, 5, result_item) + + def update_solvent_name(self): + if self.table.rowCount() > 0 and self.table.item(0, 0): + self.table.item(0, 0).setText(self.solvent_input.text()) + + def add_new_row(self): + row = self.table.rowCount() + self.table.insertRow(row) + self.table.setItem(row, 0, QTableWidgetItem(f"Реагент_{row}")) + self.table.setItem(row, 1, QTableWidgetItem("0")) + unit_combo = QComboBox() + unit_combo.addItems(["нг", "мкг", "мг", "г", "кг", "нл", "мкл", "мл", "л"]) + unit_combo.setCurrentText("мг") + self.table.setCellWidget(row, 2, unit_combo) + self.table.setItem(row, 3, QTableWidgetItem("1")) + self.table.setItem(row, 4, QTableWidgetItem("1")) + self.table.setItem(row, 5, QTableWidgetItem("")) + + def remove_selected_row(self): + for row in sorted(set(item.row() for item in self.table.selectedItems()), reverse=True): + if row > 0: + self.table.removeRow(row) + + def get_table_data(self): + data = [] + for row in range(1, self.table.rowCount()): + name = self.table.item(row, 0).text() if self.table.item(row, 0) else "" + percent = self.table.item(row, 1).text() if self.table.item(row, 1) else "0" + unit_widget = self.table.cellWidget(row, 2) + unit = unit_widget.currentText() if unit_widget else "мг" + coeff = self.table.item(row, 3).text() if self.table.item(row, 3) else "1" + dilution = self.table.item(row, 4).text() if self.table.item(row, 4) else "1" + data.append([name, percent, unit, coeff, dilution]) + return data + + def update_solvent_percent(self, solvent_percent: float): + if self.table.rowCount() > 0 and self.table.item(0, 1): + self.table.item(0, 1).setText(self.format_number(solvent_percent)) + + def update_solvent_result(self, solvent_amount: float, unit: str): + if self.table.rowCount() > 0: + if self.table.item(0, 5): + self.table.item(0, 5).setText(self.format_number(solvent_amount)) + self.table.item(0, 5).setBackground(QColor(220, 255, 220)) + if self.table.item(0, 2): + self.table.item(0, 2).setText(unit) + + def update_results(self, results: list): + for row, amount in enumerate(results, start=1): + if row < self.table.rowCount() and self.table.item(row, 5): + self.table.item(row, 5).setText(self.format_number(amount)) + self.table.item(row, 5).setBackground(QColor(220, 255, 220)) + + def clear_results(self): + for row in range(self.table.rowCount()): + if self.table.item(row, 5): + self.table.item(row, 5).setText("") + if row == 0: + self.table.item(row, 5).setBackground(QColor(230, 230, 230)) + else: + self.table.item(row, 5).setBackground(QColor(250, 250, 250)) + if self.table.rowCount() > 0 and self.table.item(0, 1): + self.table.item(0, 1).setText("") + if self.table.rowCount() > 0 and self.table.item(0, 2): + self.table.item(0, 2).setText(self.amount_unit_combo.currentText()) + + def format_number(self, value): + if value == int(value): + return str(int(value)) + formatted = f"{value:.6f}".rstrip('0').rstrip('.') + if '.' in formatted and len(formatted.split('.')[1]) > 4: + formatted = f"{value:.4f}".rstrip('0').rstrip('.') + return formatted + + def show_error(self, message: str): + QMessageBox.critical(self, "Ошибка", message) diff --git a/view.py b/view.py deleted file mode 100644 index f08d0f2..0000000 --- a/view.py +++ /dev/null @@ -1,422 +0,0 @@ -from PyQt5.QtWidgets import (QMainWindow, QVBoxLayout, QHBoxLayout, - QTableWidget, QTableWidgetItem, QPushButton, - QLabel, QDoubleSpinBox, QComboBox, QLineEdit, - QWidget, QMessageBox, QGroupBox, QFrame, QHeaderView) -from PyQt5.QtCore import Qt -from PyQt5.QtGui import QFont, QColor - - -class MediumCalculatorWindow(QMainWindow): - def __init__(self): - super().__init__() - self.setWindowTitle("Калькулятор питательных сред - Цифровой помощник биохимика") - self.setGeometry(200, 100, 1200, 700) - self.setStyleSheet(""" - QMainWindow { - background-color: #f5f5f5; - } - QGroupBox { - font-weight: bold; - border: 2px solid #ccc; - border-radius: 5px; - margin-top: 10px; - padding-top: 10px; - background-color: white; - } - QGroupBox::title { - subcontrol-origin: margin; - left: 10px; - padding: 0 5px 0 5px; - color: #1565C0; - } - QTableWidget { - gridline-color: #ddd; - background-color: white; - alternate-background-color: #f9f9f9; - } - QHeaderView::section { - background-color: #1565C0; - color: white; - padding: 8px; - border: none; - font-weight: bold; - } - QPushButton { - background-color: #2196F3; - color: white; - border: none; - padding: 8px 15px; - border-radius: 4px; - font-weight: bold; - } - QPushButton:hover { - background-color: #1976D2; - } - QPushButton:pressed { - background-color: #0D47A1; - } - QPushButton#danger { - background-color: #f44336; - } - QPushButton#danger:hover { - background-color: #da190b; - } - QPushButton#success { - background-color: #4CAF50; - } - QPushButton#success:hover { - background-color: #45a049; - } - QDoubleSpinBox, QLineEdit { - padding: 4px; - border: 1px solid #ccc; - border-radius: 3px; - background-color: white; - color: black; - font-size: 12px; - min-height: 20px; - } - QComboBox { - border: 1px solid #ccc; - border-radius: 3px; - background-color: white; - color: black; - font-size: 12px; - min-height: 20px; - padding: 2px; - } - QComboBox::drop-down { - border: none; - width: 20px; - } - QComboBox QAbstractItemView { - background-color: white; - color: black; - selection-background-color: #2196F3; - selection-color: white; - } - QLabel { - color: black; - font-size: 12px; - } - QLabel#title { - font-size: 18px; - font-weight: bold; - color: #0D47A1; - } - QLabel#info { - color: #1565C0; - font-size: 11px; - } - """) - self._init_ui() - - def _init_ui(self): - central_widget = QWidget() - self.setCentralWidget(central_widget) - layout = QVBoxLayout(central_widget) - layout.setSpacing(15) - layout.setContentsMargins(20, 20, 20, 20) - - title_label = QLabel("Калькулятор питательных сред") - title_label.setObjectName("title") - title_label.setAlignment(Qt.AlignCenter) - title_font = QFont() - title_font.setPointSize(18) - title_font.setBold(True) - title_label.setFont(title_font) - layout.addWidget(title_label) - - params_group = QGroupBox("Параметры среды") - params_layout = QHBoxLayout() - params_layout.setSpacing(20) - - amount_layout = QHBoxLayout() - amount_layout.addWidget(QLabel("Общее количество:")) - self.amount_input = QDoubleSpinBox() - self.amount_input.setRange(0.001, 1000000.0) - self.amount_input.setValue(1000.0) - self.amount_input.setMinimumWidth(150) - amount_layout.addWidget(self.amount_input) - - self.amount_unit_combo = QComboBox() - self.amount_unit_combo.addItems(["нл", "мкл", "мл", "л"]) - self.amount_unit_combo.setCurrentText("мл") - self.amount_unit_combo.setMinimumWidth(80) - amount_layout.addWidget(self.amount_unit_combo) - params_layout.addLayout(amount_layout) - - solvent_layout = QHBoxLayout() - solvent_layout.addWidget(QLabel("Растворитель:")) - self.solvent_input = QLineEdit("Вода") - self.solvent_input.setMinimumWidth(150) - solvent_layout.addWidget(self.solvent_input) - params_layout.addLayout(solvent_layout) - - params_layout.addStretch() - params_group.setLayout(params_layout) - layout.addWidget(params_group) - - table_group = QGroupBox("Состав среды") - table_layout = QVBoxLayout() - - self.table = QTableWidget() - self.table.setColumnCount(6) - self.table.setHorizontalHeaderLabels(["Название", "%", "Единица", "Коэфф.", "Разбавление (x)", "Количество"]) - self.table.setAlternatingRowColors(True) - self.table.setSelectionBehavior(QTableWidget.SelectRows) - self.table.setEditTriggers(QTableWidget.DoubleClicked | QTableWidget.EditKeyPressed) - - self.table.verticalHeader().setVisible(False) - - self.table.setColumnWidth(0, 180) - self.table.setColumnWidth(1, 70) - self.table.setColumnWidth(2, 90) - self.table.setColumnWidth(3, 70) - self.table.setColumnWidth(4, 100) - self.table.setColumnWidth(5, 120) - - table_layout.addWidget(self.table) - table_group.setLayout(table_layout) - layout.addWidget(table_group) - - btn_group = QGroupBox("Управление") - btn_layout = QHBoxLayout() - btn_layout.setSpacing(10) - - self.add_row_btn = QPushButton("Добавить реагент") - self.add_row_btn.setMinimumWidth(150) - btn_layout.addWidget(self.add_row_btn) - - self.remove_row_btn = QPushButton("Удалить реагент") - self.remove_row_btn.setObjectName("danger") - self.remove_row_btn.setMinimumWidth(150) - btn_layout.addWidget(self.remove_row_btn) - - btn_layout.addStretch() - - self.calculate_btn = QPushButton("Рассчитать") - self.calculate_btn.setObjectName("success") - self.calculate_btn.setMinimumWidth(150) - btn_layout.addWidget(self.calculate_btn) - - self.save_btn = QPushButton("Сохранить") - self.save_btn.setMinimumWidth(150) - btn_layout.addWidget(self.save_btn) - - self.load_btn = QPushButton("Загрузить") - self.load_btn.setMinimumWidth(150) - btn_layout.addWidget(self.load_btn) - - btn_group.setLayout(btn_layout) - layout.addWidget(btn_group) - - info_frame = QFrame() - info_frame.setFrameShape(QFrame.StyledPanel) - info_frame.setStyleSheet("background-color: #e3f2fd; border-radius: 5px;") - info_layout = QHBoxLayout(info_frame) - - info_label = QLabel("Подсказка: Реагенты в массовых единицах (г, мг и т.д.) не учитываются при расчёте объёма растворителя") - info_label.setObjectName("info") - info_layout.addWidget(info_label) - info_layout.addStretch() - - layout.addWidget(info_frame) - - self.add_initial_rows() - - def add_initial_rows(self): - self.add_solvent_row() - self.add_new_row() - - def add_solvent_row(self): - row_count = self.table.rowCount() - self.table.insertRow(row_count) - self.table.setRowHeight(row_count, 30) - - solvent_name = self.solvent_input.text() - solvent_item = QTableWidgetItem(solvent_name) - solvent_item.setFlags(solvent_item.flags() & ~Qt.ItemIsEditable) - solvent_item.setBackground(QColor(230, 230, 230)) - solvent_item.setForeground(QColor(0, 0, 0)) - font = QFont() - font.setBold(True) - solvent_item.setFont(font) - self.table.setItem(row_count, 0, solvent_item) - - percent_item = QTableWidgetItem("") - percent_item.setFlags(percent_item.flags() & ~Qt.ItemIsEditable) - percent_item.setBackground(QColor(230, 230, 230)) - percent_item.setForeground(QColor(0, 0, 0)) - self.table.setItem(row_count, 1, percent_item) - - unit_item = QTableWidgetItem(self.amount_unit_combo.currentText()) - unit_item.setFlags(unit_item.flags() & ~Qt.ItemIsEditable) - unit_item.setBackground(QColor(230, 230, 230)) - unit_item.setForeground(QColor(0, 0, 0)) - self.table.setItem(row_count, 2, unit_item) - - coeff_item = QTableWidgetItem("-") - coeff_item.setFlags(coeff_item.flags() & ~Qt.ItemIsEditable) - coeff_item.setBackground(QColor(230, 230, 230)) - coeff_item.setForeground(QColor(0, 0, 0)) - self.table.setItem(row_count, 3, coeff_item) - - dilution_item = QTableWidgetItem("-") - dilution_item.setFlags(dilution_item.flags() & ~Qt.ItemIsEditable) - dilution_item.setBackground(QColor(230, 230, 230)) - dilution_item.setForeground(QColor(0, 0, 0)) - self.table.setItem(row_count, 4, dilution_item) - - result_item = QTableWidgetItem("") - result_item.setFlags(result_item.flags() & ~Qt.ItemIsEditable) - result_item.setBackground(QColor(240, 240, 240)) - result_item.setForeground(QColor(0, 0, 0)) - self.table.setItem(row_count, 5, result_item) - - def update_solvent_name(self): - solvent_name = self.solvent_input.text() - name_item = self.table.item(0, 0) - if name_item: - name_item.setText(solvent_name) - - def format_number(self, value): - if value == int(value): - return str(int(value)) - else: - formatted = f"{value:.6f}".rstrip('0').rstrip('.') - if '.' in formatted and len(formatted.split('.')[1]) > 4: - formatted = f"{value:.4f}".rstrip('0').rstrip('.') - return formatted - - def add_new_row(self): - row_count = self.table.rowCount() - self.table.insertRow(row_count) - self.table.setRowHeight(row_count, 30) - - name_item = QTableWidgetItem(f"Реагент_{row_count}") - name_item.setForeground(QColor(0, 0, 0)) - self.table.setItem(row_count, 0, name_item) - - percent_item = QTableWidgetItem("0") - percent_item.setForeground(QColor(0, 0, 0)) - self.table.setItem(row_count, 1, percent_item) - - unit_combo = QComboBox() - unit_combo.addItems(["нг", "мкг", "мг", "г", "кг", "нл", "мкл", "мл", "л"]) - unit_combo.setCurrentText("мг") - self.table.setCellWidget(row_count, 2, unit_combo) - - coeff_item = QTableWidgetItem("1") - coeff_item.setForeground(QColor(0, 0, 0)) - self.table.setItem(row_count, 3, coeff_item) - - dilution_item = QTableWidgetItem("1") - dilution_item.setForeground(QColor(0, 0, 0)) - dilution_item.setFlags(dilution_item.flags() | Qt.ItemIsEditable) - self.table.setItem(row_count, 4, dilution_item) - - result_item = QTableWidgetItem("") - result_item.setFlags(result_item.flags() & ~Qt.ItemIsEditable) - result_item.setBackground(QColor(250, 250, 250)) - result_item.setForeground(QColor(0, 0, 0)) - self.table.setItem(row_count, 5, result_item) - - def remove_selected_row(self): - selected_rows = set() - for item in self.table.selectedItems(): - selected_rows.add(item.row()) - - for row in sorted(selected_rows, reverse=True): - if row > 0: - self.table.removeRow(row) - - def get_table_data(self) -> list: - data = [] - for row in range(1, self.table.rowCount()): - row_data = [] - - name_item = self.table.item(row, 0) - row_data.append(name_item.text() if name_item else "") - - percent_item = self.table.item(row, 1) - row_data.append(percent_item.text() if percent_item else "0") - - unit_widget = self.table.cellWidget(row, 2) - if unit_widget and isinstance(unit_widget, QComboBox): - row_data.append(unit_widget.currentText()) - else: - row_data.append("мг") - - coeff_item = self.table.item(row, 3) - row_data.append(coeff_item.text() if coeff_item else "1") - - dilution_item = self.table.item(row, 4) - if dilution_item: - try: - dilution_factor = float(dilution_item.text()) - except ValueError: - dilution_factor = 1.0 - row_data.append(dilution_factor) - else: - row_data.append(1.0) - - data.append(row_data) - return data - - def update_solvent_percent(self, solvent_percent: float): - percent_item = self.table.item(0, 1) - if percent_item: - percent_item.setText(self.format_number(solvent_percent)) - - def show_error(self, message: str): - QMessageBox.critical(self, "Ошибка", message) - - def update_results(self, results: list): - for row, amount in enumerate(results, start=1): - if row < self.table.rowCount(): - formatted_amount = self.format_number(amount) - result_item = QTableWidgetItem(formatted_amount) - result_item.setFlags(result_item.flags() & ~Qt.ItemIsEditable) - result_item.setBackground(QColor(220, 255, 220)) - result_item.setForeground(QColor(0, 0, 0)) - font = QFont() - font.setBold(True) - result_item.setFont(font) - self.table.setItem(row, 5, result_item) - - def update_solvent_result(self, solvent_amount: float, unit: str): - formatted_amount = self.format_number(solvent_amount) - result_item = self.table.item(0, 5) - if result_item: - result_item.setText(formatted_amount) - result_item.setBackground(QColor(220, 255, 220)) - result_item.setForeground(QColor(0, 0, 0)) - - unit_item = self.table.item(0, 2) - if unit_item: - unit_item.setText(unit) - - def update_display(self, solvent: str, total_amount: float, amount_unit: str): - self.solvent_input.setText(solvent) - self.update_solvent_name() - self.amount_input.setValue(total_amount) - self.amount_unit_combo.setCurrentText(amount_unit) - - def clear_results(self): - for row in range(self.table.rowCount()): - result_item = self.table.item(row, 5) - if result_item: - result_item.setText("") - if row == 0: - result_item.setBackground(QColor(230, 230, 230)) - else: - result_item.setBackground(QColor(250, 250, 250)) - - percent_item = self.table.item(0, 1) - if percent_item: - percent_item.setText("") - - unit_item = self.table.item(0, 2) - if unit_item: - unit_item.setText(self.amount_unit_combo.currentText())