studpy 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
studpy/__init__.py ADDED
@@ -0,0 +1,2 @@
1
+ from.study import Study
2
+ __all__=['Study']
@@ -0,0 +1,3 @@
1
+ from.batch_exec_cmd_iter import BatchExecCmdIter
2
+ from.validated_batch import ValidatedBatchExecCmdIter
3
+ __all__=['BatchExecCmdIter','ValidatedBatchExecCmdIter']
@@ -0,0 +1,32 @@
1
+ import asyncio
2
+ from abc import ABC,abstractmethod
3
+ from studpy.utils.logger_print import LoggerPrint
4
+ class BatchExecCmdIter(ABC):
5
+ logger=LoggerPrint();out_err_keywords:list[str]|None=None
6
+ @abstractmethod
7
+ def get_nb_runs(self):...
8
+ @abstractmethod
9
+ def cmd_callable(self,index_run):...
10
+ def pre_callable(A,index_run):0
11
+ def post_callable(A,index_run,returncode,stdout,stderr):0
12
+ def end_callable(A,nb_errors):0
13
+ def exec(A,max_parallel_jobs=5):
14
+ B=A.get_nb_runs();A.exec_nb_runs=B
15
+ if A.logger:A.logger.info(f"[{A.__class__.__name__}] START — {B} runs")
16
+ asyncio.run(A._main_batch(nb_runs=B,max_parallel_jobs=max_parallel_jobs))
17
+ async def _main_batch(A,nb_runs,max_parallel_jobs):
18
+ C=asyncio.Semaphore(max_parallel_jobs);D=[A._run_cmd(C,B)for B in range(nb_runs)];E=await asyncio.gather(*D);B=sum(1 for A in E if A);A.end_callable(B)
19
+ if A.logger:A.logger.info(f"[{A.__class__.__name__}] END — {B} errors / {A.exec_nb_runs} runs")
20
+ A.exec_nb_errors=B
21
+ async def _run_cmd(A,sem,index_run):
22
+ B=index_run;I=A.cmd_callable(B)
23
+ async with sem:
24
+ A.pre_callable(B)
25
+ if A.logger:A.logger.info(f"[{A.__class__.__name__}] RUN {B+1}/{A.exec_nb_runs}")
26
+ F=await asyncio.create_subprocess_shell(I,stdout=asyncio.subprocess.PIPE,stderr=asyncio.subprocess.PIPE);J,K=await F.communicate();C=F.returncode;G=J.decode().strip();H=K.decode().strip();D=None
27
+ if C!=0:D=f"return code {C} != 0"
28
+ elif A.out_err_keywords:
29
+ L=G.lower();M=H.lower()
30
+ for E in A.out_err_keywords:
31
+ if E.lower()in L or E.lower()in M:D=f"keyword '{E}' found in stdout/stderr";break
32
+ N=A.post_callable(B,C,G,H);return D or N
@@ -0,0 +1,25 @@
1
+ from __future__ import annotations
2
+ import shutil
3
+ from abc import abstractmethod
4
+ from pathlib import Path
5
+ from.batch_exec_cmd_iter import BatchExecCmdIter
6
+ class ValidatedBatchExecCmdIter(BatchExecCmdIter):
7
+ def __init__(A):A.failed_runs=[];A.success_runs=[]
8
+ @abstractmethod
9
+ def expected_output_path(self,index_run):...
10
+ def cleanup_path(A,index_run):0
11
+ def post_failure(E,index_run,returncode,stdout,stderr,reasons):
12
+ B=reasons;A=index_run;print(f" [FAIL] run {A} {' | '.join(B)}");C=stdout+stderr
13
+ if C.strip():
14
+ for D in C.strip().splitlines()[:15]:print(f" │ {D}")
15
+ return f"run {A} failed: {', '.join(B)}"
16
+ def post_callable(B,index_run,returncode,stdout,stderr):
17
+ A=index_run;C=B.expected_output_path(A);E=C.exists()and C.stat().st_size>0
18
+ if not E:
19
+ F=[f"missing/empty output: {C.name}"];D=B.cleanup_path(A)
20
+ if D and D.exists():shutil.rmtree(D)
21
+ B.failed_runs.append(A);return B.post_failure(A,returncode,stdout,stderr,F)
22
+ print(f" [OK] run {A} → {C.name}");B.success_runs.append(A)
23
+ def end_callable(A,nb_errors):
24
+ B=A.get_nb_runs();C=len(A.failed_runs)/B if B>0 else 0;print(f"\n {len(A.success_runs)} OK, {len(A.failed_runs)} failed ({C*100:.1f}%)")
25
+ if A.failed_runs:print(f" Failed indices: {A.failed_runs}")
@@ -0,0 +1,5 @@
1
+ from.configs import GaussianConfig,LHSConfig,LinearConfig,ListConfig,RandomIntConfig,TableConfig
2
+ from.latin_hypercube_sampling import LatinHypercubeSampling
3
+ from.param import Param
4
+ from.specs import ParamsSpecs
5
+ __all__=['ParamsSpecs','Param','GaussianConfig','LinearConfig','ListConfig','LHSConfig','RandomIntConfig','TableConfig','LatinHypercubeSampling']
@@ -0,0 +1,35 @@
1
+ _D='linear'
2
+ _C='gaussian'
3
+ _B='random_int'
4
+ _A=None
5
+ from typing import Any,Literal
6
+ import numpy as np
7
+ from pandas import DataFrame
8
+ from pydantic import BaseModel,model_validator
9
+ class LHSConfig(BaseModel):type:Literal['lhs']='lhs';lhs_reference:str;low:float;high:float
10
+ class TableConfig(BaseModel):type:Literal['table']='table';table_reference:str;col:str
11
+ class RandomIntConfig(BaseModel):
12
+ type:Literal[_B]=_B;low:int;high:int;size:int
13
+ def get_size(A):return A.size
14
+ def get_dataframe(A,col_name='value'):B=np.random.randint(A.low,A.high+1,size=A.size);return DataFrame(B,columns=[col_name])
15
+ class GaussianConfig(BaseModel):
16
+ type:Literal[_C]=_C;mean:float;std:float;size:int;low:float|_A=_A;high:float|_A=_A;seed:int|_A=_A
17
+ def get_size(A):return A.size
18
+ def get_value(A,index):return .0
19
+ class ListConfig(BaseModel):
20
+ type:Literal['list']='list';values:list[Any]
21
+ def get_size(A):return len(A.values)
22
+ def get_value(A,index):return A.values[index]
23
+ class LinearConfig(BaseModel):
24
+ type:Literal[_D]=_D;size:int|_A=_A;low:float;high:float;step_value:float|_A=_A
25
+ @model_validator(mode='before')
26
+ @classmethod
27
+ def resolve_missing_field(cls,values):
28
+ G='step_value';F='size';A=values;B=A.get(F);D=A.get('low');E=A.get('high');C=A.get(G)
29
+ if B is _A and C is _A:raise ValueError("'size' or 'step_value' should be defined")
30
+ if B is not _A and C is not _A:raise ValueError("only 'size' or 'step_value' should be defined")
31
+ if C is _A:A[G]=(E-D)/(B-1)
32
+ if B is _A:A[F]=round((E-D)/C)+1
33
+ return A
34
+ def get_size(A):return A.size
35
+ def get_dataframe(A,col_name='value'):B=np.arange(A.low,A.high+A.step_value,A.step_value)[:A.size];return DataFrame(B,columns=[col_name])
@@ -0,0 +1,17 @@
1
+ import random
2
+ from pandas import DataFrame
3
+ from.configs import LHSConfig
4
+ from.param import Param
5
+ class LatinHypercubeSampling:
6
+ def __init__(A,n_samples,random_seed=42):A.n_samples=n_samples;A.random_seed=random_seed
7
+ def build_cases(D,parameters,col_names):
8
+ E=parameters;B=D.n_samples;F=random.Random(D.random_seed);G={}
9
+ for A in E:H=list(range(B));F.shuffle(H);G[A.target]=H
10
+ I=[]
11
+ for K in range(B):
12
+ J={}
13
+ for A in E:
14
+ if not isinstance(A.config,LHSConfig):raise Exception('parameter should be LHSConfig')
15
+ C=A.config;L=G[A.target][K];M=(L+F.random())/B;N=col_names[A.target];J[N]=round(C.low+M*(C.high-C.low),6)
16
+ I.append(J)
17
+ return DataFrame(I)
studpy/params/param.py ADDED
@@ -0,0 +1,4 @@
1
+ from typing import Annotated
2
+ from pydantic import BaseModel,Field
3
+ from.configs import GaussianConfig,LHSConfig,LinearConfig,ListConfig,RandomIntConfig,TableConfig
4
+ class Param(BaseModel):target:str;config:Annotated[GaussianConfig|LinearConfig|ListConfig|LHSConfig|RandomIntConfig|TableConfig,Field(discriminator='type')];name:str|None=None
studpy/params/specs.py ADDED
@@ -0,0 +1,18 @@
1
+ _D='other'
2
+ _C=None
3
+ _B='table'
4
+ _A='lhs'
5
+ import math
6
+ from pandas import DataFrame
7
+ from.latin_hypercube_sampling import LatinHypercubeSampling
8
+ from.param import Param
9
+ NONAME_PREFIX='Col '
10
+ class ParamsSpecs:
11
+ def __init__(A,parameters,tables=_C,lhs=_C):A.parameters=parameters;A.tables=tables;A.lhs=lhs;A._params_by_category={_B:[A for A in A.parameters if A.config.type==_B],_A:[A for A in A.parameters if A.config.type==_A],_D:[A for A in A.parameters if A.config.type not in[_B,_A]]};A._references={_B:list(set([A.config.table_reference for A in A._params_by_category[_B]])),_A:list(set([A.config.lhs_reference for A in A._params_by_category[_A]]))}
12
+ def cases_count(A):return math.prod([A.tables[B].shape[0]for B in A._references[_B]]+[A.lhs[B].n_samples for B in A._references[_A]]+[A.config.get_size()for A in A._params_by_category[_D]])
13
+ def params_targets_names(A):return{A.target:A.name or f"{NONAME_PREFIX}{B}"for(B,A)in enumerate(A.parameters)}
14
+ def build_cases(B):
15
+ G='cross';D=B.params_targets_names();A=_C
16
+ for E in B._references[_A]:H=[A for A in B._params_by_category[_A]if A.config.lhs_reference==E];C=B.lhs[E].build_cases(H,D);A=C if A is _C else A.merge(C,how=G)
17
+ for F in B._params_by_category[_D]:C=F.config.get_dataframe(D[F.target]);A=C if A is _C else A.merge(C,how=G)
18
+ return A
studpy/study.py ADDED
@@ -0,0 +1,9 @@
1
+ from pathlib import Path
2
+ from.params import ParamsSpecs
3
+ from.targets import TargetsOneFile
4
+ class Study:
5
+ def __init__(A,specs,targets):B=specs;A.specs=B;A.cases=B.build_cases();A.targets=targets
6
+ def cases_count(A):return len(A.cases)
7
+ def cases_save(A,p):A.cases.to_csv(p,index=False)
8
+ def case_targets_values(A,case_index):B=A.cases.iloc[case_index].to_dict();C=A.specs.params_targets_names();return{A:B[C]for(A,C)in C.items()}
9
+ def case_build_inputs(A,case_index):B=case_index;C=A.case_targets_values(B);A.targets.build(case_index=B,case_targets_values=C)
@@ -0,0 +1,2 @@
1
+ from.one_file import TargetsOneFile
2
+ __all__=['TargetsOneFile']
@@ -0,0 +1,16 @@
1
+ import json,shutil
2
+ from pathlib import Path
3
+ from typing import Any
4
+ from.utils import dict_set_nested
5
+ class TargetsOneFile:
6
+ def __init__(A,output_folder,template_file,subfoldername=None):A.output_folder=output_folder;A.template_file=template_file;A.subfoldername=subfoldername
7
+ def build(A,case_index,case_targets_values):
8
+ D=A.template_file.name;C=A.output_folder/f"{case_index}";C.mkdir()
9
+ if A.subfoldername:(C/A.subfoldername).mkdir();B=C/A.subfoldername/D
10
+ else:B=C/D
11
+ shutil.copy(A.template_file,B);F=B.suffix.lower()
12
+ if F=='.json':
13
+ E=json.loads(B.read_text())
14
+ for(G,H)in case_targets_values.items():dict_set_nested(E,G,H)
15
+ I=json.dumps(E);B.write_text(I)
16
+ else:raise Exception('template type not known')
@@ -0,0 +1,5 @@
1
+ from typing import Any
2
+ def dict_set_nested(data,dict_path,value):
3
+ A=data;B=dict_path.split('.')
4
+ for C in B[:-1]:A=A.setdefault(C,{})
5
+ A[B[-1]]=value
@@ -0,0 +1,6 @@
1
+ from typing import Any
2
+ class LoggerPrint:
3
+ def info(A,msg):print(f"[INFO] {msg}")
4
+ def error(A,msg):print(f"[ERROR] {msg}")
5
+ def warning(A,msg):print(f"[WARNING] {msg}")
6
+ def debug(A,msg):print(f"[DEBUG] {msg}")
@@ -0,0 +1,20 @@
1
+ Metadata-Version: 2.4
2
+ Name: studpy
3
+ Version: 0.1.0
4
+ Summary: A collection of useful functionalities for scientific studies.
5
+ Author-email: PinkSalmon <dev@pinksalmon.cloud>
6
+ Classifier: Programming Language :: Python :: 3
7
+ Classifier: Programming Language :: Python :: 3.11
8
+ Classifier: Programming Language :: Python :: 3.12
9
+ Classifier: Programming Language :: Python :: 3.13
10
+ Requires-Python: >=3.11
11
+ Requires-Dist: pandas
12
+ Requires-Dist: pydantic
13
+ Provides-Extra: dev
14
+ Requires-Dist: mypy>=1.0; extra == 'dev'
15
+ Requires-Dist: pytest-cov>=4.0; extra == 'dev'
16
+ Requires-Dist: pytest>=7.0; extra == 'dev'
17
+ Requires-Dist: ruff>=0.4; extra == 'dev'
18
+ Provides-Extra: docs
19
+ Requires-Dist: mkdocstrings-python; extra == 'docs'
20
+ Requires-Dist: zensical; extra == 'docs'
@@ -0,0 +1,17 @@
1
+ studpy/__init__.py,sha256=YBs_bmmbPMsmLqtDHqNvqqI2Z2JdghQOhMtfCc76dt8,41
2
+ studpy/study.py,sha256=NsLsLS8M_7RPk4GU526phkrhJYuF8itDmdML4dYtbFE,558
3
+ studpy/batch/__init__.py,sha256=N2tl5q3lPuNLPQskrLZWEPDLlvkM8VvEV5Wcj8e-B0U,159
4
+ studpy/batch/batch_exec_cmd_iter.py,sha256=Pd28LcHZ1q0pb2Vx7ISgE2Ao7q5Qs5ml0zn5a9tAMEw,1613
5
+ studpy/batch/validated_batch.py,sha256=etMHSeikOIwp-2UqC_bt0CKEZ45HT7AcTsu7o11ej-Q,1251
6
+ studpy/params/__init__.py,sha256=xi70KbcvDWpKJMUFE8TSk-1QG3ODms-DEPQmEOhn-18,356
7
+ studpy/params/configs.py,sha256=2OItCi6SdNhaMrPdAk4qF-gfYPih3CMlUq7oKHfc32k,1666
8
+ studpy/params/latin_hypercube_sampling.py,sha256=1QqZ8IgIYW2O0uhoTxf_11B61mnujkXLb64AS0p6Q4Q,669
9
+ studpy/params/param.py,sha256=63PtPXQQx6LHvcGL7Y8uCLltcpJrfgRa91qjjubwbbs,338
10
+ studpy/params/specs.py,sha256=XaoABLufpTiAFYIJtzwCaFYa-G9wMeTzeUWdx7Eacn4,1302
11
+ studpy/targets/__init__.py,sha256=s-SxrZIIhJp4b0NxPQmhVWWTxSUeNO4HCpg8GS0pcII,62
12
+ studpy/targets/one_file.py,sha256=_gXd7wIoF6x7dsvIXHE5oZo6Tw3EnlANZENr-6Z0HxY,725
13
+ studpy/targets/utils.py,sha256=7w3zEfu7hQZZFq1zHy7gfhysbCl0on_w1l3A9R_eXMs,150
14
+ studpy/utils/logger_print.py,sha256=9LNtL61ind3P54i-c2vgi2oh-xHLR4pUOXCkV6MISag,211
15
+ studpy-0.1.0.dist-info/METADATA,sha256=495Yx-87_EO1HSdRPM_S78wKX70LlAd7Wmp1yyAk5_A,747
16
+ studpy-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
17
+ studpy-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any