Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 12 additions & 18 deletions .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,36 +12,30 @@ on:

jobs:
build:

runs-on: ubuntu-latest
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
python-version: [ "3.10", "3.11", "3.12" ]
python-version: [ "3.12", "3.13", "3.14" ]

steps:
- uses: actions/checkout@v4

- uses: actions/checkout@v6
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}

- name: Install poetry
run: |
python -m pip install --upgrade pip
python -m pip install poetry
- name: Install Poetry
uses: snok/install-poetry@v1
with:
version: latest
virtualenvs-create: true
virtualenvs-in-project: true

- name: Install dependencies
run: |
poetry install --with dev

# - name: Lint with flake8
# run: |
# # stop the build if there are Python syntax errors or undefined names
# flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
# flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
poetry install --with dev --no-interaction

- name: Test with pytest (coverage)
run: |
poetry run pytest -q --cov=pythonQEPest --cov-report=term-missing --cov-fail-under=40
Expand Down
608 changes: 244 additions & 364 deletions poetry.lock

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
[project]
name = "pythonQEPest"
version = "1.1.5"
description = "The rewritten version of Java QEPest"
version = "1.4.0"
description = "Java QEPest in Python"
readme = "README.md"
requires-python = ">=3.10,<3.13"
requires-python = ">=3.12,<3.15"
authors = [{ name = "Lina", email = "knocker767@gmail.com" }]
dependencies = ["pydantic==2.12.5", "python-dotenv>=1.2.1,<2.0.0"]

Expand Down
73 changes: 19 additions & 54 deletions pythonQEPest/cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,60 +6,12 @@
from pathlib import Path
from typing import Sequence

from pythonQEPest.core import QEPestMeta
from pythonQEPest.dto import QEPestFile
from pythonQEPest.helpers import get_num_of_cols
from pythonQEPest.helpers.get_values_from_line import get_values_from_line
from pythonQEPest.services.QEPestFileService import QEPestFileService

logger = logging.getLogger(__name__)


class CLI:
qepest: QEPestMeta | None = None

def __init__(self, qepest: QEPestMeta, qepest_file: QEPestFile):
self.qepest = qepest
self.qepest_file = qepest_file

def read_file_and_compute_params(self):
try:
with open(self.qepest_file.input_file, "r") as file:
lines = file.readlines()

with open(self.qepest_file.output_file, "w") as wr:
for index, line in enumerate(lines):
if index == 0:
if get_num_of_cols(line) != self.qepest.col_number:
er = f"Error: Line {index} does not have seven elements."
logger.error(er)
self.qepest.noError = False
break
wr.write("Name QEH QEI QEF\n")
else:
if get_num_of_cols(line) == self.qepest.col_number:
d_values = get_values_from_line(line.split("\t"))
self.qepest.get_qex_values(d_values)
splitted_line = line.split("\t")[0]
wr.write(
f"{splitted_line} {self.qepest.qex.qe_h} "
+ f"{self.qepest.qex.qe_i} {self.qepest.qex.qe_f}"
+ f"{chr(10)}"
)
else:
er = f"Error: Line {index} does not have seven elements."
logger.error(er)
self.qepest.noError = False
if self.qepest.noError:
logger.info("Computation completed")
else:
logger.warning("Finished with errors")

except FileNotFoundError as e:
self.qepest.noError = False
logger.error("Error: can't find : %s", self.qepest_file.input_file)
logger.exception(e)


def _resolve_package_version() -> str:
try:
return version("pythonQEPest")
Expand Down Expand Up @@ -99,10 +51,17 @@ def build_parser() -> argparse.ArgumentParser:
parser.add_argument(
"-o",
"--output",
default="data.txt.out",
default="data.out.txt",
help="Path to output tab-separated file with "
+ "QEPest descriptors (default: data.txt.out).",
)

parser.add_argument(
"-f",
"--format",
default="txt",
help="Format to output file with " + "QEPest (json, txt).",
)
return parser


Expand All @@ -117,10 +76,16 @@ def main(argv: Sequence[str] | None = None) -> int:
load_dotenv()
init_logger()

cli = CLI(
logger.debug("CLI initiated")
logger.info(f"CLI args: {argv}")

service = QEPestFileService(
qepest=QEPest(),
qepest_file=QEPestFile(input_file=args.input, output_file=args.output),
qepest_file=QEPestFile(
input_file=args.input, output_file=args.output, format=args.format
),
)
cli.read_file_and_compute_params()

return 0 if cli.qepest and cli.qepest.noError else 1
service.read_file_and_compute_params()

return 0 if not service.error else 1
7 changes: 7 additions & 0 deletions pythonQEPest/config/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from .qepest_default import qepest_default
from .normalise import normalise_default

__all__ = [
"normalise_default",
"qepest_default",
]
12 changes: 12 additions & 0 deletions pythonQEPest/config/normalise.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
normalise_default = {
"herb": (69.5849922, 94.4228257, 120.4572352, 228.1589796, 89.7012502, 276.9634213),
"insect": (
78.2919965,
71.2829691,
133.9224801,
331.170104,
70.5540709,
193.0023343,
),
"fung": (53.3719946, 52.773116, 73.7976536, 144.9887053, 41.4385926, 102.3024319),
}
26 changes: 26 additions & 0 deletions pythonQEPest/config/qepest_default.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
qepest_default = {
"herb": [
(70.77, 283.0, 84.97, -1.185), # mwH
(93.81, 3.077, 1.434, 0.6164), # logpH
(117.6, 2.409, 1.567, 7.155), # hbaH
(233.4, 0.4535, -1.48, 4.47), # hbdH
(84.7, 4.758, -2.423, 5.437), # rbH
(301.8, 1.101, 0.8869, -22.81), # arRCH
],
"insect": [
(76.38, 298.3, 83.64, 1.912), # mwI
(74.27, 4.555, -2.193, -2.987), # logpI
(139.4, 1.363, 1.283, 0.5341), # hbaI
(670.6, -1.163, 0.7856, 0.7951), # hbdI
(65.49, 6.219, -2.448, 5.318), # rbI
(287.5, 0.305, 1.554, -88.64), # arRCI
],
"fung": [
(51.03, 314.2, -56.31, 2.342), # mwF
(50.73, 3.674, -1.238, 2.067), # logpF
(73.79, 1.841, 1.326, 0.5158), # hbaF
(164.7, -0.9762, -2.027, 1.384), # hbdF
(40.91, 1.822, 2.582, 0.6235), # rbF
(134.4, 0.8383, 1.347, -31.17), # arRCF
],
}
142 changes: 97 additions & 45 deletions pythonQEPest/core/qepest.py
Original file line number Diff line number Diff line change
@@ -1,74 +1,126 @@
import logging
import math
from typing import Optional, List

from pydantic import create_model, BaseModel

from pythonQEPest.core.qepest_meta import QEPestMeta
from pythonQEPest.dto.QEPestData import QEPestData
from pythonQEPest.dto.QEPestInput import QEPestInput
from pythonQEPest.dto.QEPestOutput import QEPestOutput
from pythonQEPest.dto.normalisation.Normaliser import Normaliser
from pythonQEPest.helpers.check_nan import check_nan
from pythonQEPest.helpers.compute_df import compute_df
from pythonQEPest.helpers.get_values_from_line import get_values_from_line
from pythonQEPest.helpers.norm import norm_h, norm_f, norm_i
from pythonQEPest.helpers.round_to_4digs import round_to_4digs

logger = logging.getLogger(__name__)


class QEPest(QEPestMeta):

def __init__(self, *args, **kwargs):
self.coefficients_names = None
self.names = None

logger.debug("QEPest initialisation")

logger.info(f"QEPest args: {args}")
logger.info(f"QEPest kwargs: {kwargs}")

super().__init__(*args, **kwargs)

logger.debug("QEPest initialisation successful")

def _log_compute_df(self, func, index, lst, data_lst) -> float:
df_result = compute_df(lst[index], *data_lst[index])
return math.log(func(df_result, index))

def get_names(self) -> List[str]:
self.names = [
n.split("_")[1] for n in dir(self) if n.startswith("coefficient_")
]
return self.names

def get_coefficients_names(self) -> List[str]:
self.coefficients_names = [f"coefficient_{name}" for name in self.names]
return self.coefficients_names

def compute_params(self, data_input: QEPestInput) -> QEPestOutput:
self.get_qex_values(
get_values_from_line(list(data_input.model_dump().values()))
)
return QEPestOutput(data=self.qex, name=data_input.name)

def get_qex_values(self, d) -> None:
def log_compute_df(func, index, lst, data_lst) -> float:
df_result = compute_df(lst[index], *data_lst[index])
return math.log(func(df_result, index))
def get_qex_values(self, d) -> BaseModel:
names = self.get_names()

# Coefficients names = ("coefficients_fung, coefficient_herb...)
coefficients_names = self.get_coefficients_names()

qe_h = 0.0
qe_i = 0.0
qe_f = 0.0
if len(coefficients_names) == 0:
raise ValueError(
"No coefficient_ keys, needs to call "
+ "initialize_coefficient and workable config to work"
)

# coefficients = [{i: getattr(self, i)} for i in coefficients_names]

# Splitting by _ to get (fung, herb, etc...) to form qe_fung, qe_herb...
# qe_lst = []
for z in names:
name = f"qe_{z}"
setattr(self, name, 0.0)

d_num = len(d)
for i in range(d_num):
qe_h += log_compute_df(func=norm_h, index=i, lst=d, data_lst=self.herb)
qe_i += log_compute_df(func=norm_i, index=i, lst=d, data_lst=self.insect)
qe_f += log_compute_df(func=norm_f, index=i, lst=d, data_lst=self.fung)
for name in names:
self.__dict__[f"qe_{name}"] += self._log_compute_df(
func=self.__dict__[f"normaliser_{name}"].norm,
index=i,
lst=d,
data_lst=self.__dict__[f"coefficient_{name}"],
)

q = [
round_to_4digs(math.exp(qe_h / d_num)),
round_to_4digs(math.exp(qe_i / d_num)),
round_to_4digs(math.exp(qe_f / d_num)),
round_to_4digs(math.exp(self.__dict__[f"qe_{name}"] / d_num))
for name in names
]

result = check_nan(q)
self.qex = QEPestData(qe_h=result[0], qe_i=result[1], qe_f=result[2])

def initialize_coefficients(self) -> None:
coefficients = {
"herb": [
(70.77, 283.0, 84.97, -1.185), # mwH
(93.81, 3.077, 1.434, 0.6164), # logpH
(117.6, 2.409, 1.567, 7.155), # hbaH
(233.4, 0.4535, -1.48, 4.47), # hbdH
(84.7, 4.758, -2.423, 5.437), # rbH
(301.8, 1.101, 0.8869, -22.81), # arRCH
],
"insect": [
(76.38, 298.3, 83.64, 1.912), # mwI
(74.27, 4.555, -2.193, -2.987), # logpI
(139.4, 1.363, 1.283, 0.5341), # hbaI
(670.6, -1.163, 0.7856, 0.7951), # hbdI
(65.49, 6.219, -2.448, 5.318), # rbI
(287.5, 0.305, 1.554, -88.64), # arRCI
],
"fung": [
(51.03, 314.2, -56.31, 2.342), # mwF
(50.73, 3.674, -1.238, 2.067), # logpF
(73.79, 1.841, 1.326, 0.5158), # hbaF
(164.7, -0.9762, -2.027, 1.384), # hbdF
(40.91, 1.822, 2.582, 0.6235), # rbF
(134.4, 0.8383, 1.347, -31.17), # arRCF
],
}

fields = {f"qe_{name}": (float, 0.0) for name in names}
dynamic_model = create_model("QEPestData", **fields)
self.qex = dynamic_model(
**{f"qe_{names[idx]}": name for idx, name in enumerate(result)}
)

return self.qex

# TODO: Coefficients must be in other class.
# TODO: Ability to provide whatever we want is a good thingy
def initialize_coefficients(self, coefficients: Optional = None) -> None:
logger.debug("QEPest coefficients initialisation")
coefficients = super().initialize_coefficients()

logger.debug("QEPest coefficients initialisation successful")
for category, data in coefficients.items():
getattr(self, category).extend(data)
setattr(self, f"coefficient_{category}", data)

logger.info(f"QEPest coefficients initialisation with {coefficients.items()}")

# TODO: Same with Normalisers
def initialize_normalisers(self, normalisers: Optional = None) -> None:
logger.debug("QEPest normalisers initialisation")
normalisers = super().initialize_normalisers()

logger.debug("QEPest normalisers initialisation successful")

for category, data in normalisers.items():
setattr(self, f"normaliser_{category}", Normaliser(data))

logger.info(f"QEPest normalisers initialisation with {normalisers.items()}")


if __name__ == "__main__":
qepest = QEPest()
qepest.get_qex_values(1)
Loading